nice good refactoring to make the practice and test modes work properly

This commit is contained in:
lingdocs 2022-09-05 18:05:40 +04:00
parent 5d8888634f
commit 07ea0a286a
13 changed files with 934 additions and 683 deletions

View File

@ -17,6 +17,7 @@
"bootstrap": "4.5.3", "bootstrap": "4.5.3",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"cron": "^1.8.2", "cron": "^1.8.2",
"froebel": "^0.21.3",
"lokijs": "^1.5.12", "lokijs": "^1.5.12",
"markdown-to-jsx": "^7.1.3", "markdown-to-jsx": "^7.1.3",
"react": "^17.0.2", "react": "^17.0.2",

View File

@ -23,33 +23,187 @@ import ReactGA from "react-ga";
import { isProd } from "../lib/isProd"; import { isProd } from "../lib/isProd";
import autoAnimate from "@formkit/auto-animate"; import autoAnimate from "@formkit/auto-animate";
const errorVibration = 200; const errorVibration = 200;
const strikesToFail = 3;
const maxStrikes = 2; type GameState<Question> = ({
mode: "practice",
showAnswer: boolean,
} | {
mode: "intro" | "test" | "fail" | "timeout" | "complete",
showAnswer: false,
}) & {
numberComplete: number,
current: Question,
timerKey: number,
strikes: number,
justStruck: boolean,
}
function GameCore<T>({ inChapter, questions, Display, timeLimit, Instructions, studyLink, id }:{ type GameReducerAction = {
type: "handle question response",
payload: { correct: boolean },
} | {
type: "start",
payload: "practice" | "test",
} | {
type: "quit",
} | {
type: "timeout",
} | {
type: "show answer",
} | {
type: "skip",
}
function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCorrectAnswer, timeLimit, Instructions, studyLink, id }: {
inChapter: boolean, inChapter: boolean,
id: string, id: string,
studyLink: string, studyLink: string,
Instructions: (props: { opts?: Types.TextOptions }) => JSX.Element, Instructions: (props: { opts?: Types.TextOptions }) => JSX.Element,
questions: () => QuestionGenerator<T>, getQuestion: () => Question,
Display: (props: QuestionDisplayProps<T>) => JSX.Element, DisplayCorrectAnswer: (props: { question: Question }) => JSX.Element,
timeLimit: number; Display: (props: QuestionDisplayProps<Question>) => JSX.Element,
timeLimit: number,
amount: number,
}) { }) {
const initialState: GameState<Question> = {
mode: "intro",
numberComplete: 0,
current: getQuestion(),
timerKey: 0,
strikes: 0,
justStruck: false,
showAnswer: false,
};
// TODO: report pass with id to user info // TODO: report pass with id to user info
const rewardRef = useRef<RewardElement | null>(null); const rewardRef = useRef<RewardElement | null>(null);
const parent = useRef<HTMLDivElement | null>(null); const parent = useRef<HTMLDivElement | null>(null);
const { user, pullUser, setUser } = useUser(); const { user, pullUser, setUser } = useUser();
const [mode, setMode] = useState<"practice" | "test">("test"); const [state, setStateDangerous] = useState<GameState<Question>>(initialState);
const [finish, setFinish] = useState<undefined | "pass" | { msg: "fail", answer: JSX.Element } | "time out">(undefined);
const [strikes, setStrikes] = useState<number>(0);
const [justStruck, setJustStruck] = useState<boolean>(false);
const [current, setCurrent] = useState<Current<T> | undefined>(undefined);
const [questionBox, setQuestionBox] = useState<QuestionGenerator<T>>(questions());
const [timerKey, setTimerKey] = useState<number>(1);
useEffect(() => { useEffect(() => {
parent.current && autoAnimate(parent.current) parent.current && autoAnimate(parent.current)
}, [parent]); }, [parent]);
const gameReducer = (gs: GameState<Question>, action: GameReducerAction): GameState<Question> => {
if (action.type === "handle question response") {
if (gs.mode === "test") {
if (action.payload.correct) {
const numberComplete = gs.numberComplete + 1;
if (numberComplete === amount) {
logGameEvent("passed");
rewardRef.current?.rewardMe();
handleResult(true);
return {
...gs,
numberComplete,
justStruck: false,
mode: "complete",
}
} else {
return {
...gs,
numberComplete,
current: getQuestion(),
justStruck: false,
};
}
} else {
punish();
const strikes = gs.strikes + 1;
if (strikes === strikesToFail) {
logGameEvent("fail");
handleResult(false);
return {
...gs,
strikes,
mode: "fail",
justStruck: false,
};
} else {
return {
...gs,
strikes,
justStruck: true,
};
}
}
}
else /* (gs.mode === "practice") */ {
if (action.payload.correct) {
const numberComplete = gs.numberComplete + 1;
return {
...gs,
numberComplete,
current: getQuestion(),
justStruck: false,
showAnswer: false,
};
} else {
punish();
const strikes = gs.strikes + 1;
return {
...gs,
strikes,
justStruck: true,
showAnswer: false,
};
}
}
}
if (action.type === "start") {
logGameEvent(`started ${action.payload}`);
return {
...initialState,
mode: action.payload,
current: getQuestion(),
timerKey: gs.timerKey + 1,
}
}
if (action.type === "quit") {
return {
...initialState,
timerKey: gs.timerKey + 1,
}
}
if (action.type === "timeout") {
logGameEvent("timeout");
handleResult(false);
return {
...gs,
mode: "timeout",
justStruck: false,
showAnswer: false,
};
}
if (action.type === "show answer") {
if (gs.mode === "practice" && gs.justStruck) {
return {
...gs,
justStruck: false,
showAnswer: true,
};
}
return gs;
}
if (action.type === "skip") {
if (gs.mode === "practice") {
return {
...gs,
current: getQuestion(),
justStruck: false,
showAnswer: false,
};
}
return gs;
}
throw new Error("unknown GameReducerAction");
}
function dispatch(action: GameReducerAction) {
setStateDangerous(gs => gameReducer(gs, action));
}
function logGameEvent(action: string) { function logGameEvent(action: string) {
if (isProd && !(user?.admin)) { if (isProd && !(user?.admin)) {
ReactGA.event({ ReactGA.event({
@ -59,35 +213,15 @@ function GameCore<T>({ inChapter, questions, Display, timeLimit, Instructions, s
}); });
} }
} }
function punish() {
function handleCallback(correct: true | JSX.Element) {
if (correct === true) {
handleAdvance();
return;
}
setStrikes(s => s + 1);
navigator.vibrate(errorVibration); navigator.vibrate(errorVibration);
if (strikes < maxStrikes) { }
setJustStruck(true); function handleResult(done: boolean) {
} else {
logGameEvent("fail on game");
setJustStruck(false);
setFinish({ msg: "fail", answer: correct });
const result: AT.TestResult = { const result: AT.TestResult = {
done: false, done,
time: getTimestamp(), time: getTimestamp(),
id, id,
}; };
handleResult(result);
}
}
function handleAdvance() {
setJustStruck(false);
const next = questionBox.next();
if (next.done) handleFinish();
else setCurrent(next.value);
}
function handleResult(result: AT.TestResult) {
// add the test to the user object // add the test to the user object
if (!user) return; if (!user) return;
setUser((u) => { setUser((u) => {
@ -105,127 +239,105 @@ function GameCore<T>({ inChapter, questions, Display, timeLimit, Instructions, s
if (r === "sent") pullUser(); if (r === "sent") pullUser();
}).catch(console.error); }).catch(console.error);
} }
function handleFinish() {
logGameEvent("passed game")
setFinish("pass");
rewardRef.current?.rewardMe();
if (!user) return;
const result: AT.TestResult = {
done: true,
time: getTimestamp(),
id,
};
handleResult(result);
}
function handleQuit() {
setFinish(undefined);
setCurrent(undefined);
}
function handleRestart(mode: "test" | "practice") {
logGameEvent(`started game ${mode}`);
setMode(mode);
const newQuestionBox = questions();
const { value } = newQuestionBox.next();
// just for type safety -- the generator will have at least one question
if (!value) return;
setQuestionBox(newQuestionBox);
setJustStruck(false);
setStrikes(0);
setFinish(undefined);
setCurrent(value);
setTimerKey(prev => prev + 1);
}
function handleTimeOut() {
logGameEvent("timeout on game");
setJustStruck(false);
setFinish("time out");
navigator.vibrate(errorVibration);
const result: AT.TestResult = {
done: false,
time: getTimestamp(),
id,
};
handleResult(result);
}
function getProgressWidth(): string { function getProgressWidth(): string {
const num = !current const num = !state.current
? 0 ? 0
: (finish === "pass") : (state.mode === "complete")
? 100 ? 100
: getPercentageDone(current.progress); : getPercentageDone(state.numberComplete, amount);
return `${num}%`; return `${num}%`;
} }
const progressColor = finish === "pass" const progressColor = state.mode === "complete"
? "success" ? "success"
: typeof finish === "object" : (state.mode === "fail" || state.mode === "timeout")
? "danger" ? "danger"
: "primary"; : "primary";
const gameRunning = current && finish === undefined; const gameRunning = state.mode === "practice" || state.mode === "test";
function ActionButtons() { function ActionButtons() {
return <div> return <div>
{!inChapter && <Link to={studyLink}> {!inChapter && <Link to={studyLink}>
<button className="btn btn-danger mt-4 mx-3">Study</button> <button className="btn btn-danger mt-4 mx-3">Study</button>
</Link>} </Link>}
<button className="btn btn-warning mt-4 mx-3" onClick={() => handleRestart("practice")}>Practice</button> <button className="btn btn-warning mt-4 mx-3" onClick={() => dispatch({ type: "start", payload: "practice" })}>Practice</button>
<button className="btn btn-success mt-4 mx-3" onClick={() => handleRestart("test")}>Test</button> <button className="btn btn-success mt-4 mx-3" onClick={() => dispatch({ type: "start", payload: "test" })}>Test</button>
</div>; </div>;
} }
return <> return <>
<div className="text-center" style={{ minHeight: "200px", zIndex: 10, position: "relative" }}> <div className="text-center" style={{ minHeight: "200px", zIndex: 10, position: "relative" }}>
{mode === "test" && <div className="progress" style={{ height: "5px" }}> {(state.mode === "test" || state.mode === "intro") && <div className="progress" style={{ height: "5px" }}>
<div className={`progress-bar bg-${progressColor}`} role="progressbar" style={{ width: getProgressWidth() }} /> <div className={`progress-bar bg-${progressColor}`} role="progressbar" style={{ width: getProgressWidth() }} />
</div>} </div>}
{current && <div className="d-flex flex-row justify-content-between mt-2"> <div className="d-flex flex-row justify-content-between mt-2">
<StrikesDisplay strikes={strikes} /> {state.mode === "test" && <StrikesDisplay strikes={state.strikes} />}
<div className="d-flex flex-row-reverse"> {state.mode === "practice" && <PracticeStatusDisplay
{mode === "test" && <CountdownCircleTimer correct={state.numberComplete}
key={timerKey} incorrect={state.strikes}
isPlaying={!!current && !finish} />}
<div className="d-flex flex-row justify-content-right">
{state.mode === "test" && <CountdownCircleTimer
key={state.timerKey}
isPlaying={gameRunning}
size={30} size={30}
colors={["#555555", "#F7B801", "#A30000"]} colors={["#555555", "#F7B801", "#A30000"]}
colorsTime={[timeLimit, timeLimit*0.33, 0]} colorsTime={[timeLimit, timeLimit*0.33, 0]}
strokeWidth={4} strokeWidth={4}
strokeLinecap="square" strokeLinecap="square"
duration={timeLimit} duration={timeLimit}
onComplete={handleTimeOut} onComplete={() => dispatch({ type: "timeout" })}
/>} />}
<button onClick={handleQuit} className="btn btn-outline-secondary btn-sm mr-2">Quit</button> {state.mode !== "intro" && <button onClick={() => dispatch({ type: "quit" })} className="btn btn-outline-secondary btn-sm ml-2">
Quit
</button>}
</div> </div>
</div>} </div>
{mode === "test" && <div ref={parent}> <div ref={parent}>
{justStruck && <div className="alert alert-warning my-2" role="alert" style={{ maxWidth: "300px", margin: "0 auto" }}> {state.justStruck && <div className="alert alert-warning my-2" role="alert" style={{ maxWidth: "300px", margin: "0 auto" }}>
{getStrikeMessage()} {getStrikeMessage()}
</div>} </div>}
</div>} </div>
<Reward ref={rewardRef} config={{ lifetime: 130, spread: 90, elementCount: 150, zIndex: 999999999 }} type="confetti"> <Reward ref={rewardRef} config={{ lifetime: 130, spread: 90, elementCount: 150, zIndex: 999999999 }} type="confetti">
<div> <div>
{finish === undefined && {state.mode === "intro" && <div>
(current
? <div>
<Display question={current.question} callback={handleCallback} />
</div>
: <div>
<div className="pt-3"> <div className="pt-3">
{/* TODO: ADD IN TEXT DISPLAY OPTIONS HERE TOO - WHEN WE START USING THEM*/} {/* TODO: ADD IN TEXT DISPLAY OPTIONS HERE TOO - WHEN WE START USING THEM*/}
<Instructions /> <Instructions />
</div> </div>
<ActionButtons /> <ActionButtons />
</div>) </div>}
} {gameRunning && <Display
{finish === "pass" && <div> question={state.current}
callback={(correct) => dispatch({ type: "handle question response", payload: { correct }})}
/>}
{(state.mode === "practice" && state.justStruck) && <div className="my-3">
<button className="btn btn-sm btn-secondary" onClick={() => dispatch({ type: "show answer" })}>
Show Answer
</button>
</div>}
{(state.showAnswer && state.mode === "practice") && <div className="my-2">
<div>The correct answer was:</div>
<div className="my-1">
<DisplayCorrectAnswer question={state.current} />
</div>
<button className="btn btn-sm btn-primary my-2" onClick={() => dispatch({ type: "skip" })}>
Next Question
</button>
</div>}
{state.mode === "complete" && <div>
<h4 className="mt-4"> <h4 className="mt-4">
<span role="img" aria-label="celebration">🎉</span> Finished! <span role="img" aria-label="celebration">🎉</span> Finished!
</h4> </h4>
<button className="btn btn-secondary mt-4" onClick={() => handleRestart("test")}>Try Again</button> <button className="btn btn-secondary mt-4" onClick={() => dispatch({ type: "start", payload: "test" })}>Try Again</button>
</div>} </div>}
{(typeof finish === "object" || finish === "time out") && <div> {(state.mode === "timeout" || state.mode === "fail") && <div className="mb-4">
{mode === "test" && <h4 className="mt-4">{failMessage(current?.progress, finish)}</h4>} <h4 className="mt-4">{failMessage({
{typeof finish === "object" && <div> numberComplete: state.numberComplete,
amount,
type: state.mode,
})}</h4>
<div>The correct answer was:</div> <div>The correct answer was:</div>
<div className="my-2"> <div className="my-2">
{finish?.answer} <DisplayCorrectAnswer question={state.current} />
</div> </div>
</div>}
<div className="my-3"> <div className="my-3">
<ActionButtons /> <ActionButtons />
</div> </div>
@ -246,6 +358,13 @@ function GameCore<T>({ inChapter, questions, Display, timeLimit, Instructions, s
</>; </>;
} }
function PracticeStatusDisplay({ correct, incorrect }: { correct: number, incorrect: number }) {
return <div className="d-flex flex-row justify-content-between align-items-center small">
<div className="mr-3"> <samp>Correct: {correct}</samp></div>
<div> <samp>Incorrect: {incorrect}</samp></div>
</div>
}
function StrikesDisplay({ strikes }: { strikes: number }) { function StrikesDisplay({ strikes }: { strikes: number }) {
return <div> return <div>
{[...Array(strikes)].map(_ => <span key={Math.random()} className="mr-2"></span>)} {[...Array(strikes)].map(_ => <span key={Math.random()} className="mr-2"></span>)}
@ -262,8 +381,12 @@ function getStrikeMessage() {
]); ]);
} }
function failMessage(progress: Progress | undefined, finish: "time out" | { msg: "fail", answer: JSX.Element }): string { function failMessage({ numberComplete, amount, type }: {
const pDone = progress ? getPercentageDone(progress) : 0; numberComplete: number,
amount: number,
type: "timeout" | "fail",
}): string {
const pDone = getPercentageDone(numberComplete, amount);
const { message, face } = pDone < 20 const { message, face } = pDone < 20
? { message: "No, sorry", face: "😑" } ? { message: "No, sorry", face: "😑" }
: pDone < 30 : pDone < 30
@ -273,7 +396,7 @@ function failMessage(progress: Progress | undefined, finish: "time out" | { msg:
: pDone < 78 : pDone < 78
? { message: "You almost got it!", face: "😩" } ? { message: "You almost got it!", face: "😩" }
: { message: "Nooo! So close!", face: "😭" }; : { message: "Nooo! So close!", face: "😭" };
return typeof finish === "object" return type === "fail"
? `${message} ${face}` ? `${message} ${face}`
: `⏳ Time's Up ${face}`; : `⏳ Time's Up ${face}`;
} }

View File

@ -2,6 +2,8 @@ import EquativeGame from "./sub-cores/EquativeGame";
import VerbGame from "./sub-cores/VerbGame"; import VerbGame from "./sub-cores/VerbGame";
import GenderGame from "./sub-cores/GenderGame"; import GenderGame from "./sub-cores/GenderGame";
import UnisexNounGame from "./sub-cores/UnisexNounGame"; import UnisexNounGame from "./sub-cores/UnisexNounGame";
import EquativeSituations from "./sub-cores/EquativeSituations";
import EquativeIdentify from "./sub-cores/EquativeIdentify";
// NOUNS // NOUNS
export const nounGenderGame1 = makeGameRecord({ export const nounGenderGame1 = makeGameRecord({
@ -87,21 +89,21 @@ export const equativeGameAllIdentify = makeGameRecord({
title: "Identify the equative (all tenses)", title: "Identify the equative (all tenses)",
id: "equative-past-summary-identify", id: "equative-past-summary-identify",
link: "/equatives/other-equatives", link: "/equatives/other-equatives",
level: "allIdentify", level: "allTenses",
SubCore: EquativeGame, SubCore: EquativeIdentify,
}); });
export const equativeGameSituations = makeGameRecord({ export const equativeGameSituations = makeGameRecord({
title: "Choose the right equative for the situation", title: "Choose the right equative for the situation",
id: "equative-past-situations", id: "equative-past-situations",
link: "/equatives/other-equatives", link: "/equatives/other-equatives",
level: "situations", level: "situations",
SubCore: EquativeGame, SubCore: EquativeSituations,
}); });
export const equativeGameAllProduce = makeGameRecord({ export const equativeGameAllProduce = makeGameRecord({
title: "Write the equative (all tenses)", title: "Write the equative (all tenses)",
id: "equative-past-summary-produce", id: "equative-past-summary-produce",
link: "/equatives/other-equatives", link: "/equatives/other-equatives",
level: "allProduce", level: "allTenses",
SubCore: EquativeGame, SubCore: EquativeGame,
}); });

View File

@ -1,173 +1,22 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
comparePs, comparePs,
makeProgress,
} from "../../lib/game-utils"; } from "../../lib/game-utils";
import GameCore from "../GameCore"; import GameCore from "../GameCore";
import { import {
Types as T, Types as T,
Examples, Examples,
defaultTextOptions as opts, defaultTextOptions as opts,
typePredicates as tp,
makeNounSelection,
randFromArray,
renderEP, renderEP,
compileEP, compileEP,
flattenLengths, flattenLengths,
randomPerson,
InlinePs, InlinePs,
grammarUnits, grammarUnits,
} from "@lingdocs/pashto-inflector"; } from "@lingdocs/pashto-inflector";
import { psStringEquals } from "@lingdocs/pashto-inflector/dist/lib/p-text-helpers"; import { randomEPSPool } from "./makeRandomEPS";
const kidsColor = "#017BFE"; const kidsColor = "#017BFE";
// @ts-ignore
const nouns: T.NounEntry[] = [
{"ts":1527815251,"i":7790,"p":"سړی","f":"saRéy","g":"saRey","e":"man","c":"n. m.","ec":"man","ep":"men"},
{"ts":1527812797,"i":8605,"p":"ښځه","f":"xúdza","g":"xudza","e":"woman, wife","c":"n. f.","ec":"woman","ep":"women"},
{"ts":1527812881,"i":11691,"p":"ماشوم","f":"maashoom","g":"maashoom","e":"child, kid","c":"n. m. anim. unisex","ec":"child","ep":"children"},
{"ts":1527815197,"i":2503,"p":"پښتون","f":"puxtoon","g":"puxtoon","e":"Pashtun","c":"n. m. anim. unisex / adj.","infap":"پښتانه","infaf":"puxtaanu","infbp":"پښتن","infbf":"puxtan"},
{"ts":1527815737,"i":484,"p":"استاذ","f":"Ustaaz","g":"Ustaaz","e":"teacher, professor, expert, master (in a field)","c":"n. m. anim. unisex anim.","ec":"teacher"},
{"ts":1527816747,"i":6418,"p":"ډاکټر","f":"DaakTar","g":"DaakTar","e":"doctor","c":"n. m. anim. unisex"},
{"ts":1527812661,"i":13938,"p":"هلک","f":"halík, halúk","g":"halik,haluk","e":"boy, young lad","c":"n. m. anim."},
].filter(tp.isNounEntry);
// @ts-ignore
const adjectives: T.AdjectiveEntry[] = [
{"ts":1527815306,"i":7582,"p":"ستړی","f":"stúRey","g":"stuRey","e":"tired","c":"adj."},
{"ts":1527812625,"i":9116,"p":"غټ","f":"ghuT, ghaT","g":"ghuT,ghaT","e":"big, fat","c":"adj."},
{"ts":1527812792,"i":5817,"p":"خوشاله","f":"khoshaala","g":"khoshaala","e":"happy, glad","c":"adj."},
{"ts":1527812796,"i":8641,"p":"ښه","f":"xu","g":"xu","e":"good","c":"adj."},
{"ts":1527812798,"i":5636,"p":"خفه","f":"khúfa","g":"khufa","e":"sad, upset, angry; choked, suffocated","c":"adj."},
{"ts":1527822049,"i":3610,"p":"تکړه","f":"takRá","g":"takRa","e":"strong, energetic, skillful, great, competent","c":"adj."},
{"ts":1527815201,"i":2240,"p":"پټ","f":"puT","g":"puT","e":"hidden","c":"adj."},
{"ts":1527815381,"i":3402,"p":"تږی","f":"túGey","g":"tugey","e":"thirsty","c":"adj."},
{"ts":1527812822,"i":10506,"p":"کوچنی","f":"koochnéy","g":"koochney","e":"little, small; child, little one","c":"adj. / n. m. anim. unisex"},
{"ts":1527815451,"i":7243,"p":"زوړ","f":"zoR","g":"zoR","e":"old","c":"adj. irreg.","infap":"زاړه","infaf":"zaaRu","infbp":"زړ","infbf":"zaR"},
{"ts":1527812927,"i":12955,"p":"موړ","f":"moR","g":"moR","e":"full, satisfied, sated","c":"adj. irreg.","infap":"ماړه","infaf":"maaRu","infbp":"مړ","infbf":"maR"},
].filter(tp.isAdjectiveEntry);
// @ts-ignore
const locAdverbs: T.LocativeAdverbEntry[] = [
{"ts":1527812558,"i":6241,"p":"دلته","f":"dălta","g":"dalta","e":"here","c":"loc. adv."},
{"ts":1527812449,"i":13937,"p":"هلته","f":"hálta, álta","g":"halta,alta","e":"there","c":"loc. adv."},
].filter(tp.isLocativeAdverbEntry);
const tenses: T.EquativeTense[] = [
"present", "habitual", "subjunctive", "future", "past", "wouldBe", "pastSubjunctive", "wouldHaveBeen"
];
type Situation = {
description: string | JSX.Element,
tense: T.EquativeTense[],
};
const situations: Situation[] = [
{
description: <>A is B, for sure, right now</>,
tense: ["present"],
},
{
description: <>A is <em>probably</em> B, right now</>,
tense: ["future"],
},
{
description: <>A will be B in the future</>,
tense: ["future"],
},
{
description: <>We can assume that A is most likely B</>,
tense: ["future"],
},
{
description: <>You <em>know</em> A is B, currently</>,
tense: ["present"],
},
{
description: <>A tends to be B</>,
tense: ["habitual"],
},
{
description: <>A is usually B</>,
tense: ["habitual"],
},
{
description: <>A is generally B</>,
tense: ["habitual"],
},
{
description: <>A is B, right now</>,
tense: ["present"],
},
{
description: <>A is always B, as a matter of habit</>,
tense: ["present"],
},
{
description: "It's a good thing for A to be B",
tense: ["subjunctive"],
},
{
description: "A needs to be B (out of obligation/necessity)",
tense: ["subjunctive"],
},
{
description: "You hope that A is B",
tense: ["subjunctive"],
},
{
description: "You desire A to be B",
tense: ["subjunctive"],
},
{
description: "If A is B ...",
tense: ["subjunctive"],
},
{
description: "...so that A will be B (a purpose)",
tense: ["subjunctive"],
},
{
description: "A was definately B",
tense: ["past"],
},
{
description: "A was B",
tense: ["past"],
},
{
description: "A was probably B in the past",
tense: ["wouldBe"],
},
{
description: "A used to be B (habitually, repeatedly)",
tense: ["wouldBe"],
},
{
description: "assume that A would have probably been B",
tense: ["wouldBe"],
},
{
description: "under different circumstances, A would have been B",
tense: ["wouldBe", "pastSubjunctive"],
},
{
description: "You wish A were B (but it's not)",
tense: ["pastSubjunctive"],
},
{
description: "If A were B (but it's not)",
tense: ["pastSubjunctive"],
},
{
description: "Aaagh! If only A were B!",
tense: ["pastSubjunctive"],
},
{
description: "A should have been B!",
tense: ["pastSubjunctive", "wouldBe"],
},
];
const amount = 12; const amount = 12;
const timeLimit = 100; const timeLimit = 100;
@ -175,99 +24,28 @@ type Question = {
EPS: T.EPSelectionComplete, EPS: T.EPSelectionComplete,
phrase: { ps: T.PsString[], e?: string[] }, phrase: { ps: T.PsString[], e?: string[] },
equative: T.EquativeRendered, equative: T.EquativeRendered,
} | {
situation: Situation,
}; };
const pronounTypes = [ export default function EquativeGame({ inChapter, id, link, level }: { inChapter: boolean, id: string, link: string, level: T.EquativeTense | "allTenses" }) {
[T.Person.FirstSingMale, T.Person.FirstSingFemale], const epsPool = randomEPSPool(level);
[T.Person.SecondSingMale, T.Person.SecondSingFemale], function getQuestion(): Question {
[T.Person.ThirdSingMale], const EPS = epsPool();
[T.Person.ThirdSingFemale],
[T.Person.FirstPlurMale, T.Person.FirstPlurFemale],
[T.Person.SecondPlurMale, T.Person.SecondPlurFemale],
[T.Person.ThirdPlurMale, T.Person.ThirdPlurFemale],
];
export default function EquativeGame({ inChapter, id, link, level }: { inChapter: boolean, id: string, link: string, level: T.EquativeTense | "allProduce" | "allIdentify" | "situations" }) {
function* questions (): Generator<Current<Question>> {
let pool = [...pronounTypes];
let situationPool = [...situations];
function makeRandPronoun(): T.PronounSelection {
let person: T.Person;
do {
person = randomPerson();
// eslint-disable-next-line
} while (!pool.some(p => p.includes(person)));
pool = pool.filter(p => !p.includes(person));
if (pool.length === 0) {
pool = pronounTypes;
}
return {
type: "pronoun",
distance: "far",
person,
};
}
function makeRandomNoun(): T.NounSelection {
const n = makeNounSelection(randFromArray(nouns), undefined);
return {
...n,
gender: n.genderCanChange ? randFromArray(["masc", "fem"]) : n.gender,
number: n.numberCanChange ? randFromArray(["singular", "plural"]) : n.number,
};
}
function makeRandomEPS(l: T.EquativeTense | "allIdentify" | "allProduce"): T.EPSelectionComplete {
const subj: T.NPSelection = {
type: "NP",
selection: randFromArray([
makeRandPronoun,
makeRandPronoun,
makeRandomNoun,
makeRandPronoun,
])(),
};
const pred = randFromArray([...adjectives, ...locAdverbs]);
const tense = (l === "allIdentify" || l === "allProduce")
? randFromArray(tenses)
: l;
return makeEPS(subj, pred, tense);
}
for (let i = 0; i < amount; i++) {
if (level === "situations") {
const picked = randFromArray(situationPool);
situationPool = situationPool.filter(x => picked.description !== x.description);
if (situationPool.length === 0) situationPool = [...situations];
yield {
progress: makeProgress(i, amount),
question: {
situation: picked,
},
};
} else {
const EPS = makeRandomEPS(level);
const EP = renderEP(EPS); const EP = renderEP(EPS);
const compiled = compileEP( const compiled = compileEP(
EP, EP,
true, true,
level === "allIdentify" ? undefined : { equative: true, kidsSection: true }, { equative: true, kidsSection: true },
); );
const phrase = { const phrase = {
ps: compiled.ps, ps: compiled.ps,
e: level === "allIdentify" ? undefined : compiled.e, e: compiled.e,
}; };
yield { return {
progress: makeProgress(i, amount),
question: {
EPS, EPS,
phrase, phrase,
equative: getEqFromRendered(EP), equative: getEqFromRendered(EP),
},
}; };
}
}; };
}
function Display({ question, callback }: QuestionDisplayProps<Question>) { function Display({ question, callback }: QuestionDisplayProps<Question>) {
const [answer, setAnswer] = useState<string>(""); const [answer, setAnswer] = useState<string>("");
@ -276,73 +54,20 @@ export default function EquativeGame({ inChapter, id, link, level }: { inChapter
setAnswer(value); setAnswer(value);
} }
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
if ("situation" in question) {
return;
}
e.preventDefault(); e.preventDefault();
const correct = comparePs(answer, question.equative.ps) const correct = comparePs(answer, question.equative.ps)
&& (withBa === question.equative.hasBa); && (withBa === question.equative.hasBa);
if (correct) { if (correct) {
setAnswer(""); setAnswer("");
} }
callback(!correct ? makeCorrectAnswer(question) : true); callback(correct);
}
const handleTenseIdentify = (tense: T.EquativeTense) => {
if ("situation" in question) {
const wasCorrect = question.situation.tense.includes(tense);
if (wasCorrect) {
callback(true);
} else {
callback(makeCorrectAnswer(question));
}
return;
}
const renderedWAnswer = renderEP({
...question.EPS,
equative: {
...question.EPS.equative,
tense,
},
});
const compiledWAnswer = compileEP(renderedWAnswer, true);
const wasCorrect = compiledWAnswer.ps.some(a => (
question.phrase.ps.some(b => psStringEquals(a, b))
));
if (wasCorrect) {
return callback(wasCorrect);
} else {
const possibleCorrect = tenses.filter(tn => {
const r = renderEP({
...question.EPS,
equative: {
...question.EPS.equative,
tense: tn,
},
});
const c = compileEP(r, true);
return c.ps.some(a => (
question.phrase.ps.some(b => psStringEquals(a, b))
));
});
callback(<div className="lead">
{possibleCorrect.map(humanReadableTense).join(" or ")}
</div>)
}
} }
useEffect(() => { useEffect(() => {
if (level === "allProduce") setWithBa(false); if (level === "allTenses") setWithBa(false);
}, [question]); }, [question]);
return <div> return <div>
{(level === "allIdentify" || level === "situations") ?
<div className="mb-2" style={{ maxWidth: "300px", margin: "0 auto" }}> <div className="mb-2" style={{ maxWidth: "300px", margin: "0 auto" }}>
{"situation" in question ? <p className="lead">
{question.situation.description}
</p> : <Examples opts={opts}>
{randFromArray(question.phrase.ps)}
</Examples>}
</div>
: !("situation" in question) && <div className="mb-2" style={{ maxWidth: "300px", margin: "0 auto" }}>
<Examples lineHeight={1} opts={opts}> <Examples lineHeight={1} opts={opts}>
{/* @ts-ignore TODO: REMOVE AS P_INFLE */} {/* @ts-ignore TODO: REMOVE AS P_INFLE */}
{modExs(question.phrase.ps, withBa)[0]} {modExs(question.phrase.ps, withBa)[0]}
@ -352,20 +77,7 @@ export default function EquativeGame({ inChapter, id, link, level }: { inChapter
))} ))}
<div>{humanReadableTense(question.EPS.equative.tense)} equative</div> <div>{humanReadableTense(question.EPS.equative.tense)} equative</div>
</div> </div>
} <form onSubmit={handleSubmit}>
{level === "allIdentify" || "situation" in question ? <div className="text-center">
<div className="row">
{tenses.map(t => <div className="col" key={Math.random()}>
<button
style={{ width: "8rem" }}
className="btn btn-outline-secondary mb-3"
onClick={() => handleTenseIdentify(t)}
>
{humanReadableTense(t)}
</button>
</div>)}
</div>
</div> : <form onSubmit={handleSubmit}>
<div className="form-check mt-1"> <div className="form-check mt-1">
<input <input
id="baCheckbox" id="baCheckbox"
@ -392,44 +104,39 @@ export default function EquativeGame({ inChapter, id, link, level }: { inChapter
</div> </div>
<div className="text-center my-2"> <div className="text-center my-2">
{/* <div> */} {/* <div> */}
<button className="btn btn-primary" type="submit">return </button> <button className="btn btn-primary" type="submit">submit </button>
{/* </div> */} {/* </div> */}
{/* <div className="text-muted small text-center mt-2"> {/* <div className="text-muted small text-center mt-2">
Type <kbd>Enter</kbd> to check Type <kbd>Enter</kbd> to check
</div> */} </div> */}
</div> </div>
</form>} </form>
</div> </div>
} }
function Instructions() { function Instructions() {
return <div> return <div>
{level === "allIdentify" <p className="lead">
? <p className="lead">Identify a correct tense for each equative phrase you see</p> Fill in the blank with the correct {level === "allTenses" ? "" : humanReadableTense(level)} equative
: level === "situations" </p>
? <p className="lead">Choose the right type of equative for each given situation</p> {level === "allTenses" && <div> All tenses included...</div>}
: <p className="lead">Fill in the blank with the correct <strong>{humanReadableTense(level)} equative</strong> <strong>in Pashto script</strong></p>} </div>;
{level === "allProduce" && <div> All tenses included...</div>}
</div>
} }
return <GameCore return <GameCore
inChapter={inChapter} inChapter={inChapter}
studyLink={link} studyLink={link}
questions={questions} getQuestion={getQuestion}
id={id} id={id}
Display={Display} Display={Display}
timeLimit={level === "allProduce" ? timeLimit * 1.4 : timeLimit} DisplayCorrectAnswer={DisplayCorrectAnswer}
timeLimit={level === "allTenses" ? timeLimit * 1.3 : timeLimit}
amount={amount}
Instructions={Instructions} Instructions={Instructions}
/> />
}; };
function makeCorrectAnswer(question: Question): JSX.Element { function DisplayCorrectAnswer({ question }: { question: Question }): JSX.Element {
if ("situation" in question) {
return <div>
{question.situation.tense.map(humanReadableTense).join(" or ")}
</div>;
}
return <div> return <div>
<div> <div>
{flattenLengths(question.equative.ps).reduce(((accum, curr, i): JSX.Element[] => ( {flattenLengths(question.equative.ps).reduce(((accum, curr, i): JSX.Element[] => (
@ -472,38 +179,7 @@ function humanReadableTense(tense: T.EquativeTense | "allProduce"): string {
: tense; : tense;
} }
function makeEPS(subject: T.NPSelection, predicate: T.AdjectiveEntry | T.LocativeAdverbEntry, tense: T.EquativeTense): T.EPSelectionComplete {
return {
blocks: [
{
key: Math.random(),
block: {
type: "subjectSelection",
selection: subject,
},
},
],
predicate: {
type: "predicateSelection",
selection: {
type: "complement",
selection: tp.isAdjectiveEntry(predicate) ? {
type: "adjective",
entry: predicate,
sandwich: undefined,
} : {
type: "loc. adv.",
entry: predicate,
},
},
},
equative: {
tense,
negative: false,
},
omitSubject: false,
};
}
function getEqFromRendered(e: T.EPRendered): T.EquativeRendered { function getEqFromRendered(e: T.EPRendered): T.EquativeRendered {
const eblock = e.blocks[0].find(x => x.block.type === "equative"); const eblock = e.blocks[0].find(x => x.block.type === "equative");

View File

@ -0,0 +1,140 @@
import GameCore from "../GameCore";
import {
Types as T,
Examples,
defaultTextOptions as opts,
randFromArray,
renderEP,
compileEP,
} from "@lingdocs/pashto-inflector";
import { psStringEquals } from "@lingdocs/pashto-inflector/dist/lib/p-text-helpers";
import { randomEPSPool } from "./makeRandomEPS";
import { useEffect, useState } from "react";
import classNames from "classnames";
const tenses: T.EquativeTense[] = [
"present", "habitual", "subjunctive", "future", "past", "wouldBe", "pastSubjunctive", "wouldHaveBeen",
];
const amount = 12;
const timeLimit = 120;
type Question = {
EPS: T.EPSelectionComplete,
phrase: T.PsString,
possibleEquatives: T.EquativeTense[],
};
export default function EquativeIdentify({ inChapter, id, link, level }: { inChapter: boolean, id: string, link: string, level: "allTenses" }) {
const epsPool = randomEPSPool("allTenses");
function getQuestion(): Question {
const EPS = epsPool();
const EP = renderEP(EPS);
const compiled = compileEP(EP, true);
const phrase = randFromArray(compiled.ps);
return {
EPS,
phrase,
possibleEquatives: getPossibleEquatives(phrase, EPS),
};
};
function Display({ question, callback }: QuestionDisplayProps<Question>) {
console.log({ question });
const [selected, setSelected] = useState<T.EquativeTense[]>([]);
useEffect(() => {
setSelected([]);
}, [question, setSelected]);
function handleTenseClick(t: T.EquativeTense) {
setSelected(s => selected.includes(t)
? s.filter(x => x !== t)
: [...s, t],
);
}
function handleSubmitAnswer() {
const correct = (
(selected.length === question.possibleEquatives.length)
&&
(question.possibleEquatives.every(e => selected.includes(e)))
);
callback(correct);
}
return <div>
<div style={{ maxWidth: "300px", margin: "0 auto" }}>
<Examples opts={opts}>
{question.phrase}
</Examples>
</div>
<div className="text-center">
<div className="small text-muted mb-2">Select all possible tenses</div>
<div className="row">
{tenses.map(t => <div className="col" key={Math.random()}>
<button
style={{ width: "8rem" }}
className={classNames(
"btn btn-outline-secondary mb-3",
{ active: selected.includes(t) },
)}
onClick={() => handleTenseClick(t)}
>
{humanReadableTense(t)}
</button>
</div>)}
</div>
<button className="btn btn-primary mb-2" onClick={handleSubmitAnswer}>Submit</button>
</div>
</div>
}
function Instructions() {
return <div>
<p className="lead">Identify ALL the possible tenses for each equative phrase you see</p>
</div>;
}
return <GameCore
inChapter={inChapter}
studyLink={link}
getQuestion={getQuestion}
id={id}
Display={Display}
DisplayCorrectAnswer={DisplayCorrectAnswer}
timeLimit={timeLimit}
amount={amount}
Instructions={Instructions}
/>
};
function DisplayCorrectAnswer({ question }: { question: Question }): JSX.Element {
return <div className="lead">
{question.possibleEquatives.map(humanReadableTense).join(" or ")}
</div>;
}
function getPossibleEquatives(ps: T.PsString, eps: T.EPSelectionComplete): T.EquativeTense[] {
const possible = tenses.filter(tense => {
const rendered = renderEP({
...eps,
equative: {
...eps.equative,
tense,
},
});
const compiled = compileEP(rendered, true);
return compiled.ps.some(x => psStringEquals(x, ps, false));
});
if (possible.length === 0) throw new Error("no possible tenses found");
return possible;
}
function humanReadableTense(tense: T.EquativeTense | "allProduce"): string {
return tense === "allProduce"
? ""
: tense === "pastSubjunctive"
? "past subjunctive"
: tense === "wouldBe"
? `"would be"`
: tense === "wouldHaveBeen"
? `"would have been"`
: tense;
}

View File

@ -0,0 +1,199 @@
import GameCore from "../GameCore";
import {
Types as T,
} from "@lingdocs/pashto-inflector";
import { makePool } from "../../lib/pool";
const tenses: T.EquativeTense[] = [
"present", "habitual", "subjunctive", "future", "past", "wouldBe", "pastSubjunctive", "wouldHaveBeen"
];
type Situation = {
description: string | JSX.Element,
tense: T.EquativeTense[],
};
const amount = 12;
const timeLimit = 100;
const situations: Situation[] = [
{
description: <>A is B, for sure, right now</>,
tense: ["present"],
},
{
description: <>A is <em>probably</em> B, right now</>,
tense: ["future"],
},
{
description: <>A will be B in the future</>,
tense: ["future"],
},
{
description: <>We can assume that A is most likely B</>,
tense: ["future"],
},
{
description: <>You <em>know</em> A is B, currently</>,
tense: ["present"],
},
{
description: <>A tends to be B</>,
tense: ["habitual"],
},
{
description: <>A is usually B</>,
tense: ["habitual"],
},
{
description: <>A is generally B</>,
tense: ["habitual"],
},
{
description: <>A is B, right now</>,
tense: ["present"],
},
{
description: <>A is always B, as a matter of habit</>,
tense: ["present"],
},
{
description: "It's a good thing for A to be B",
tense: ["subjunctive"],
},
{
description: "A needs to be B (out of obligation/necessity)",
tense: ["subjunctive"],
},
{
description: "You hope that A is B",
tense: ["subjunctive"],
},
{
description: "You desire A to be B",
tense: ["subjunctive"],
},
{
description: "If A is B ...",
tense: ["subjunctive"],
},
{
description: "...so that A will be B (a purpose)",
tense: ["subjunctive"],
},
{
description: "A was definately B",
tense: ["past"],
},
{
description: "A was B",
tense: ["past"],
},
{
description: "A was probably B in the past",
tense: ["wouldBe"],
},
{
description: "A used to be B (habitually, repeatedly)",
tense: ["wouldBe"],
},
{
description: "assume that A would have probably been B",
tense: ["wouldBe"],
},
{
description: "under different circumstances, A would have been B",
tense: ["wouldBe", "pastSubjunctive"],
},
{
description: "You wish A were B (but it's not)",
tense: ["pastSubjunctive"],
},
{
description: "If A were B (but it's not)",
tense: ["pastSubjunctive"],
},
{
description: "Aaagh! If only A were B!",
tense: ["pastSubjunctive"],
},
{
description: "A should have been B!",
tense: ["pastSubjunctive", "wouldBe"],
},
];
type Question = Situation;
export default function EquativeSituations({ inChapter, id, link, level }: { inChapter: boolean, id: string, link: string, level: "situations" }) {
const situationsPool = makePool(situations);
function getQuestion(): Question {
return situationsPool();
};
function Display({ question, callback }: QuestionDisplayProps<Question>) {
function handleTenseClick(t: T.EquativeTense) {
callback(question.tense.includes(t));
}
return <div>
<div className="mb-2" style={{ maxWidth: "300px", margin: "0 auto" }}>
<p className="lead">
{question.description}
</p>
</div>
<div className="text-center">
<div className="row">
{tenses.map(t => <div className="col" key={Math.random()}>
<button
style={{ width: "8rem" }}
className="btn btn-outline-secondary mb-3"
onClick={() => handleTenseClick(t)}
>
{humanReadableTense(t)}
</button>
</div>)}
</div>
</div>
</div>
}
function Instructions() {
return <p className="lead">Choose a type of equative that works for each given situation</p>;
}
return <GameCore
inChapter={inChapter}
studyLink={link}
getQuestion={getQuestion}
id={id}
Display={Display}
DisplayCorrectAnswer={DisplayCorrectAnswer}
timeLimit={timeLimit}
amount={amount}
Instructions={Instructions}
/>
};
function DisplayCorrectAnswer({ question }: { question: Question }): JSX.Element {
// callback(<div className="lead">
// {possibleCorrect.map(humanReadableTense).join(" or ")}
// </div>)
return <div>
{question.tense.map(humanReadableTense).join(" or ")}
</div>;
}
function humanReadableTense(tense: T.EquativeTense | "allProduce"): string {
return tense === "allProduce"
? ""
: tense === "pastSubjunctive"
? "past subjunctive"
: tense === "wouldBe"
? `"would be"`
: tense === "wouldHaveBeen"
? `"would have been"`
: tense;
}

View File

@ -1,6 +1,3 @@
import {
makeProgress,
} from "../../lib/game-utils";
import genderColors from "../../lib/gender-colors"; import genderColors from "../../lib/gender-colors";
import GameCore from "../GameCore"; import GameCore from "../GameCore";
import { import {
@ -84,7 +81,7 @@ const exceptions: Record<string, CategorySet> = {
}, },
}; };
const amount = 35; const amount = 30;
type Question = T.DictionaryEntry; type Question = T.DictionaryEntry;
export default function GenderGame({level, id, link, inChapter }: { export default function GenderGame({level, id, link, inChapter }: {
@ -92,11 +89,9 @@ export default function GenderGame({level, id, link, inChapter }: {
level: 1 | 2, id: string, level: 1 | 2, id: string,
link: string, link: string,
}) { }) {
function* questions (): Generator<Current<Question>> {
const wordPool = {...types}; const wordPool = {...types};
const exceptionsPool = {...exceptions}; const exceptionsPool = {...exceptions};
console.log(exceptionsPool); function getQuestion(): Question {
for (let i = 0; i < amount; i++) {
const base = level === 1 const base = level === 1
? wordPool ? wordPool
: randFromArray([wordPool, exceptionsPool]); : randFromArray([wordPool, exceptionsPool]);
@ -107,23 +102,12 @@ export default function GenderGame({level, id, link, inChapter }: {
} while (!base[gender][typeToUse].length); } while (!base[gender][typeToUse].length);
const question = randFromArray(base[gender][typeToUse]); const question = randFromArray(base[gender][typeToUse]);
base[gender][typeToUse] = base[gender][typeToUse].filter((entry) => entry.ts !== question.ts); base[gender][typeToUse] = base[gender][typeToUse].filter((entry) => entry.ts !== question.ts);
yield { return question;
progress: makeProgress(i, amount),
question,
};
}
} }
function Display({ question, callback }: QuestionDisplayProps<Question>) { function Display({ question, callback }: QuestionDisplayProps<Question>) {
function check(gender: T.Gender) { function check(gender: T.Gender) {
const nounGender: T.Gender = nounNotIn(mascNouns)(question) ? "fem" : "masc"; const nounGender: T.Gender = nounNotIn(mascNouns)(question) ? "fem" : "masc";
const correct = gender === nounGender; callback(gender === nounGender);
callback(!correct
? <div className="my-2 text-center">
<button style={{ background: genderColors[nounGender === "masc" ? "m" : "f"], color: "black" }} className="btn btn-lg" disabled>
{nounGender === "masc" ? "Masculine" : "Feminine"}
</button>
</div>
: true);
} }
return <div> return <div>
<div className="mb-4" style={{ fontSize: "larger" }}> <div className="mb-4" style={{ fontSize: "larger" }}>
@ -149,12 +133,23 @@ export default function GenderGame({level, id, link, inChapter }: {
</div> </div>
} }
function DisplayCorrectAnswer({ question }: { question: Question}) {
const nounGender: T.Gender = nounNotIn(mascNouns)(question) ? "fem" : "masc";
return <div className="my-2 text-center">
<button style={{ background: genderColors[nounGender === "masc" ? "m" : "f"], color: "black" }} className="btn btn-lg" disabled>
{nounGender === "masc" ? "Masculine" : "Feminine"}
</button>
</div>;
}
return <GameCore return <GameCore
inChapter={inChapter} inChapter={inChapter}
studyLink={link} studyLink={link}
questions={questions} getQuestion={getQuestion}
id={id} id={id}
Display={Display} Display={Display}
DisplayCorrectAnswer={DisplayCorrectAnswer}
amount={amount}
timeLimit={level === 1 ? 70 : 80} timeLimit={level === 1 ? 70 : 80}
Instructions={Instructions} Instructions={Instructions}
/> />

View File

@ -1,6 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { import {
makeProgress,
comparePs, comparePs,
} from "../../lib/game-utils"; } from "../../lib/game-utils";
import genderColors from "../../lib/gender-colors"; import genderColors from "../../lib/gender-colors";
@ -28,9 +27,8 @@ const amount = 20;
type Question = { entry: T.DictionaryEntry, gender: T.Gender }; type Question = { entry: T.DictionaryEntry, gender: T.Gender };
export default function UnisexNounGame({ id, link, inChapter }: { inChapter: boolean, id: string, link: string }) { export default function UnisexNounGame({ id, link, inChapter }: { inChapter: boolean, id: string, link: string }) {
function* questions (): Generator<Current<Question>> {
let pool = { ...types }; let pool = { ...types };
for (let i = 0; i < amount; i++) { function getQuestion(): Question {
const keys = Object.keys(types) as NType[]; const keys = Object.keys(types) as NType[];
let type: NType let type: NType
do { do {
@ -43,15 +41,11 @@ export default function UnisexNounGame({ id, link, inChapter }: { inChapter: boo
const gender = randFromArray(genders) as T.Gender; const gender = randFromArray(genders) as T.Gender;
// @ts-ignore // @ts-ignore
pool[type] = pool[type].filter((x) => x.ts !== entry.ts); pool[type] = pool[type].filter((x) => x.ts !== entry.ts);
yield { return {
progress: makeProgress(i, amount),
question: {
entry, entry,
gender, gender,
},
}; };
} }
}
function Display({ question, callback }: QuestionDisplayProps<Question>) { function Display({ question, callback }: QuestionDisplayProps<Question>) {
function flipGender(g: T.Gender): T.Gender { function flipGender(g: T.Gender): T.Gender {
@ -81,14 +75,7 @@ export default function UnisexNounGame({ id, link, inChapter }: { inChapter: boo
if (correct) { if (correct) {
setAnswer(""); setAnswer("");
} }
callback(!correct callback(correct);
? <div>
{correctAnswer.length > 1 && <div className="text-muted">One of the following:</div>}
{correctAnswer.map((ps) => (
<Examples opts={opts}>{ps}</Examples>
))}
</div>
: true);
} }
return <div> return <div>
@ -128,12 +115,28 @@ export default function UnisexNounGame({ id, link, inChapter }: { inChapter: boo
</div> </div>
} }
function DisplayCorrectAnswer({ question }: { question: Question }) {
const infOut = inflectWord(question.entry);
if (!infOut) return <div>WORD ERROR</div>;
const { inflections } = infOut;
// @ts-ignore
const correctAnswer = inflections[flipGender(question.gender)][0];
return <div>
{correctAnswer.length > 1 && <div className="text-muted">One of the following:</div>}
{correctAnswer.map((ps: any) => (
<Examples opts={opts}>{ps}</Examples>
))}
</div>;
}
return <GameCore return <GameCore
inChapter={inChapter} inChapter={inChapter}
studyLink={link} studyLink={link}
questions={questions} getQuestion={getQuestion}
id={id} id={id}
Display={Display} Display={Display}
DisplayCorrectAnswer={DisplayCorrectAnswer}
amount={amount}
timeLimit={130} timeLimit={130}
Instructions={Instructions} Instructions={Instructions}
/> />

View File

@ -1,7 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { import {
comparePs, comparePs,
makeProgress,
} from "../../lib/game-utils"; } from "../../lib/game-utils";
import GameCore from "../GameCore"; import GameCore from "../GameCore";
import { import {
@ -95,6 +94,26 @@ type VerbGameLevel = {
| "futureVerb" | "imperative" | "intransitivePerfectivePast" | "futureVerb" | "imperative" | "intransitivePerfectivePast"
| "intransitiveImperfectivePast" | "transitivePerfectivePast" | "transitiveImperfectivePast"; | "intransitiveImperfectivePast" | "transitivePerfectivePast" | "transitiveImperfectivePast";
} }
type VerbPoolName = "basic" | "transitivePast" | "intransitivePast";
function selectVerbPool({ type }: VerbGameLevel): VerbPoolName {
return type === "presentVerb"
? "basic"
: type === "futureVerb"
? "basic"
: type === "subjunctiveVerb"
? "basic"
: type === "imperative"
? "basic"
: type === "intransitiveImperfectivePast"
? "intransitivePast"
: type === "intransitivePerfectivePast"
? "intransitivePast"
: type === "transitiveImperfectivePast"
? "transitivePast"
// : type === "transitivePerfectivePast"
: "transitivePast";
}
// TODO: Level where you create the formulas (seperate file) // TODO: Level where you create the formulas (seperate file)
// level where you choose the right situation // level where you choose the right situation
@ -105,21 +124,19 @@ const VerbGame: GameSubCore<VerbGameLevel> = ({ id, link, level, inChapter }: {
link: string, link: string,
level: VerbGameLevel, level: VerbGameLevel,
}) => { }) => {
function* questions (): Generator<Current<Question>> {
const personPool = makePool(level.type === "imperative" const personPool = makePool(level.type === "imperative"
? secondPersons ? secondPersons
: persons : persons
); );
const verbsUsed = level.type.startsWith("intransitive") const verbPools: Record<VerbPoolName, () => T.VerbEntry> = {
? intransitivePastVerbs basic: makePool(verbs, 15),
: level.type.startsWith("transitive") transitivePast: makePool(transitivePastVerbs, 15),
? transitivePastVerbs intransitivePast: makePool(intransitivePastVerbs, 15),
: verbs; };
const oneVerb = randFromArray(verbsUsed); const oneVerb: T.VerbEntry = verbPools[selectVerbPool(level)]();
const verbPool = makePool(verbsUsed, 15);
const getVerb = level.level === 1 const getVerb = level.level === 1
? () => oneVerb ? () => oneVerb
: () => verbPool(); : () => verbPools[selectVerbPool(level)]();
function makeRandomNoun(): T.NounSelection { function makeRandomNoun(): T.NounSelection {
const n = makeNounSelection(randFromArray(nouns), undefined); const n = makeNounSelection(randFromArray(nouns), undefined);
return { return {
@ -172,7 +189,7 @@ const VerbGame: GameSubCore<VerbGameLevel> = ({ id, link, level, inChapter }: {
: "grammatically transitive", : "grammatically transitive",
}); });
} }
for (let i = 0; i < amount; i++) { function getQuestion(): Question {
const VPS = makeRandomVPS(levelToTense(level)); const VPS = makeRandomVPS(levelToTense(level));
const VP = renderVP(VPS); const VP = renderVP(VPS);
const compiled = compileVP( const compiled = compileVP(
@ -185,13 +202,9 @@ const VerbGame: GameSubCore<VerbGameLevel> = ({ id, link, level, inChapter }: {
ps: compiled.ps, ps: compiled.ps,
e: compiled.e, e: compiled.e,
}; };
yield { return {
progress: makeProgress(i, amount),
question: {
rendered: VP, rendered: VP,
phrase, phrase,
},
};
}; };
} }
@ -208,7 +221,7 @@ const VerbGame: GameSubCore<VerbGameLevel> = ({ id, link, level, inChapter }: {
if (correct) { if (correct) {
setAnswer(""); setAnswer("");
} }
callback(!correct ? makeCorrectAnswer(question) : true); callback(correct);
} }
// useEffect(() => { // useEffect(() => {
// if (level === "allProduce") setWithBa(false); // if (level === "allProduce") setWithBa(false);
@ -264,10 +277,12 @@ const VerbGame: GameSubCore<VerbGameLevel> = ({ id, link, level, inChapter }: {
return <GameCore return <GameCore
inChapter={inChapter} inChapter={inChapter}
studyLink={link} studyLink={link}
questions={questions} getQuestion={getQuestion}
id={id} id={id}
Display={Display} Display={Display}
DisplayCorrectAnswer={DisplayCorrectAnswer}
timeLimit={timeLimit} timeLimit={timeLimit}
amount={amount}
Instructions={Instructions} Instructions={Instructions}
/> />
}; };
@ -307,7 +322,7 @@ function QuestionDisplay({ question, userAnswer }: {
</div>; </div>;
} }
function makeCorrectAnswer(question: Question): JSX.Element { function DisplayCorrectAnswer({ question }: { question: Question }): JSX.Element {
return <div> return <div>
<div> <div>
{getVerbPs(question.rendered).reduce(((accum, curr, i): JSX.Element[] => ( {getVerbPs(question.rendered).reduce(((accum, curr, i): JSX.Element[] => (

View File

@ -0,0 +1,120 @@
import {
Types as T,
typePredicates as tp,
makeNounSelection,
randFromArray,
} from "@lingdocs/pashto-inflector";
import { makePool } from "../../lib/pool";
const pronouns: T.Person[] = [
0, 1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 9, 10, 11,
];
const tenses: T.EquativeTense[] = [
"present", "habitual", "subjunctive", "future", "past", "wouldBe", "pastSubjunctive", "wouldHaveBeen"
];
// @ts-ignore
const nouns: T.NounEntry[] = [
{"ts":1527815251,"i":7790,"p":"سړی","f":"saRéy","g":"saRey","e":"man","c":"n. m.","ec":"man","ep":"men"},
{"ts":1527812797,"i":8605,"p":"ښځه","f":"xúdza","g":"xudza","e":"woman, wife","c":"n. f.","ec":"woman","ep":"women"},
{"ts":1527812881,"i":11691,"p":"ماشوم","f":"maashoom","g":"maashoom","e":"child, kid","c":"n. m. anim. unisex","ec":"child","ep":"children"},
{"ts":1527815197,"i":2503,"p":"پښتون","f":"puxtoon","g":"puxtoon","e":"Pashtun","c":"n. m. anim. unisex / adj.","infap":"پښتانه","infaf":"puxtaanu","infbp":"پښتن","infbf":"puxtan"},
{"ts":1527815737,"i":484,"p":"استاذ","f":"Ustaaz","g":"Ustaaz","e":"teacher, professor, expert, master (in a field)","c":"n. m. anim. unisex anim.","ec":"teacher"},
{"ts":1527816747,"i":6418,"p":"ډاکټر","f":"DaakTar","g":"DaakTar","e":"doctor","c":"n. m. anim. unisex"},
{"ts":1527812661,"i":13938,"p":"هلک","f":"halík, halúk","g":"halik,haluk","e":"boy, young lad","c":"n. m. anim."},
].filter(tp.isNounEntry);
// @ts-ignore
const adjectives: T.AdjectiveEntry[] = [
{"ts":1527815306,"i":7582,"p":"ستړی","f":"stúRey","g":"stuRey","e":"tired","c":"adj."},
{"ts":1527812625,"i":9116,"p":"غټ","f":"ghuT, ghaT","g":"ghuT,ghaT","e":"big, fat","c":"adj."},
{"ts":1527812792,"i":5817,"p":"خوشاله","f":"khoshaala","g":"khoshaala","e":"happy, glad","c":"adj."},
{"ts":1527812796,"i":8641,"p":"ښه","f":"xu","g":"xu","e":"good","c":"adj."},
{"ts":1527812798,"i":5636,"p":"خفه","f":"khúfa","g":"khufa","e":"sad, upset, angry; choked, suffocated","c":"adj."},
{"ts":1527822049,"i":3610,"p":"تکړه","f":"takRá","g":"takRa","e":"strong, energetic, skillful, great, competent","c":"adj."},
{"ts":1527815201,"i":2240,"p":"پټ","f":"puT","g":"puT","e":"hidden","c":"adj."},
{"ts":1527815381,"i":3402,"p":"تږی","f":"túGey","g":"tugey","e":"thirsty","c":"adj."},
{"ts":1527812822,"i":10506,"p":"کوچنی","f":"koochnéy","g":"koochney","e":"little, small; child, little one","c":"adj. / n. m. anim. unisex"},
{"ts":1527815451,"i":7243,"p":"زوړ","f":"zoR","g":"zoR","e":"old","c":"adj. irreg.","infap":"زاړه","infaf":"zaaRu","infbp":"زړ","infbf":"zaR"},
{"ts":1527812927,"i":12955,"p":"موړ","f":"moR","g":"moR","e":"full, satisfied, sated","c":"adj. irreg.","infap":"ماړه","infaf":"maaRu","infbp":"مړ","infbf":"maR"},
].filter(tp.isAdjectiveEntry);
// @ts-ignore
const locAdverbs: T.LocativeAdverbEntry[] = [
{"ts":1527812558,"i":6241,"p":"دلته","f":"dălta","g":"dalta","e":"here","c":"loc. adv."},
{"ts":1527812449,"i":13937,"p":"هلته","f":"hálta, álta","g":"halta,alta","e":"there","c":"loc. adv."},
].filter(tp.isLocativeAdverbEntry);
export function randomEPSPool(l: T.EquativeTense | "allTenses") {
const pronounPool = makePool(pronouns);
const nounPool = makePool(nouns, 20);
const predPool = makePool([...adjectives, ...locAdverbs], 20);
const tensePool = makePool(tenses, 15);
function makeRandPronoun(): T.PronounSelection {
return {
type: "pronoun",
distance: "far",
person: pronounPool(),
};
}
function makeRandomNoun(): T.NounSelection {
const n = makeNounSelection(nounPool(), undefined);
return {
...n,
gender: n.genderCanChange ? randFromArray(["masc", "fem"]) : n.gender,
number: n.numberCanChange ? randFromArray(["singular", "plural"]) : n.number,
};
}
return function makeRandomEPS(): T.EPSelectionComplete {
const subj: T.NPSelection = {
type: "NP",
selection: randFromArray([
makeRandPronoun,
makeRandPronoun,
makeRandomNoun,
makeRandPronoun,
])(),
};
const pred = predPool();
const tense = (l === "allTenses")
? tensePool()
: l;
return makeEPS(subj, pred, tense);
}
}
function makeEPS(subject: T.NPSelection, predicate: T.AdjectiveEntry | T.LocativeAdverbEntry, tense: T.EquativeTense): T.EPSelectionComplete {
return {
blocks: [
{
key: Math.random(),
block: {
type: "subjectSelection",
selection: subject,
},
},
],
predicate: {
type: "predicateSelection",
selection: {
type: "complement",
selection: tp.isAdjectiveEntry(predicate) ? {
type: "adjective",
entry: predicate,
sandwich: undefined,
} : {
type: "loc. adv.",
entry: predicate,
},
},
},
equative: {
tense,
negative: false,
},
omitSubject: false,
};
}

View File

@ -7,33 +7,12 @@ import {
flattenLengths, flattenLengths,
} from "@lingdocs/pashto-inflector"; } from "@lingdocs/pashto-inflector";
export function makeRandomQs<Q>( export function getPercentageDone(current: number, total: number): number {
amount: number,
makeQuestion: () => Q
): () => QuestionGenerator<Q> {
function makeProgress(i: number, total: number): Progress {
return { current: i + 1, total };
}
return function* () {
for (let i = 0; i < amount; i++) {
yield {
progress: makeProgress(i, amount),
question: makeQuestion(),
};
}
}
}
export function getPercentageDone(progress: Progress): number {
return Math.round( return Math.round(
(progress.current / (progress.total + 1)) * 100 (current / (total + 1)) * 100
); );
} }
export function makeProgress(i: number, total: number): Progress {
return { current: i + 1, total };
}
/** /**
* Says if an input written in phonetics by the user is correct/the same as a given answer * Says if an input written in phonetics by the user is correct/the same as a given answer
* *

View File

@ -3,11 +3,6 @@ type Progress = {
current: number, current: number,
}; };
type Current<T> = {
progress: Progress,
question: T,
};
type GameSubCore<T> = (props: { type GameSubCore<T> = (props: {
inChapter: boolean, inChapter: boolean,
id: string, id: string,
@ -15,11 +10,9 @@ type GameSubCore<T> = (props: {
link: string; link: string;
}) => JSX.Element; }) => JSX.Element;
type QuestionGenerator<T> = Generator<Current<T>, void, unknown>;
type QuestionDisplayProps<T> = { type QuestionDisplayProps<T> = {
question: T, question: T,
callback: (correct: true | JSX.Element) => void, callback: (correct: boolean) => void,
}; };
type GameRecord = { type GameRecord = {

View File

@ -5897,6 +5897,11 @@ fresh@0.5.2:
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
froebel@^0.21.3:
version "0.21.3"
resolved "https://registry.yarnpkg.com/froebel/-/froebel-0.21.3.tgz#a780c45ccd6599c3840c0b74c6e3538b6e87a566"
integrity sha512-MqyU/nwqUZULHcr7dD0vaSrt+LUEuvgF0aN8LfXbwTJysQF/s3Y0P46SKnOoiVpELftdJXdUbOZ8VReRIOG5DQ==
from2@^2.1.0: from2@^2.1.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"