some rough equative games

This commit is contained in:
lingdocs 2022-05-09 12:57:56 -05:00
parent 77b500693b
commit 2bd4a09f43
12 changed files with 424 additions and 59 deletions

View File

@ -5,7 +5,7 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4", "@fortawesome/fontawesome-free": "^5.15.4",
"@lingdocs/lingdocs-main": "^0.2.0", "@lingdocs/lingdocs-main": "^0.2.0",
"@lingdocs/pashto-inflector": "^2.4.3", "@lingdocs/pashto-inflector": "^2.4.9",
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",

View File

@ -24,6 +24,10 @@ import {
addToForm, addToForm,
InlinePs, InlinePs,
} from "@lingdocs/pashto-inflector"; } from "@lingdocs/pashto-inflector";
import {
equativeGamePresent,
} from "../../games/games";
import GameDisplay from "../../games/GameDisplay";
The [equative](https://en.wikipedia.org/wiki/Equative) might be the most basic way of joining words together. We use it to say that something *is/equals* something else. It's kind of like an equals "=" sign in math. The [equative](https://en.wikipedia.org/wiki/Equative) might be the most basic way of joining words together. We use it to say that something *is/equals* something else. It's kind of like an equals "=" sign in math.
@ -212,3 +216,5 @@ When you're asking a question, all you have to do is change the intonation. You
<Examples opts={defaultTextOptions}>{[ <Examples opts={defaultTextOptions}>{[
{ p: "خو ته یې افعان", f: "kho tu ye afgháan", e: "But you are Afghan!" } { p: "خو ته یې افعان", f: "kho tu ye afgháan", e: "But you are Afghan!" }
]}</Examples> ]}</Examples>
<GameDisplay record={equativeGamePresent} />

View File

@ -20,31 +20,30 @@ import {
} from "@lingdocs/pashto-inflector"; } from "@lingdocs/pashto-inflector";
const errorVibration = 200; const errorVibration = 200;
function GameCore<T>({ questions, Display, timeLimit, Instructions, studyLink, id }:{ function GameCore<T>({ questions, Display, timeLimit, Instructions, studyLink, id, onStartStop }:{
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>, questions: () => QuestionGenerator<T>,
Display: (props: QuestionDisplayProps<T>) => JSX.Element, Display: (props: QuestionDisplayProps<T>) => JSX.Element,
timeLimit: number; timeLimit: number;
onStartStop: (a: "start" | "stop") => void,
}) { }) {
// 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 { user, pullUser, setUser } = useUser(); const { user, pullUser, setUser } = useUser();
const [finish, setFinish] = useState<null | "pass" | "fail" | "time out">(null); const [finish, setFinish] = useState<undefined | "pass" | { msg: "fail", answer: JSX.Element } | "time out">(undefined);
const [current, setCurrent] = useState<Current<T> | undefined>(undefined); const [current, setCurrent] = useState<Current<T> | undefined>(undefined);
const [questionBox, setQuestionBox] = useState<QuestionGenerator<T>>(questions()); const [questionBox, setQuestionBox] = useState<QuestionGenerator<T>>(questions());
const [timerKey, setTimerKey] = useState<number>(1); const [timerKey, setTimerKey] = useState<number>(1);
function handleCallback(correct: boolean) { function handleCallback(correct: true | JSX.Element) {
if (correct) handleAdvance(); if (correct === true) handleAdvance();
else handleFailure(); else {
} setFinish({ msg: "fail", answer: correct });
function handleFailure() {
// rewardRef.current?.punishMe();
setFinish("fail");
navigator.vibrate(errorVibration); navigator.vibrate(errorVibration);
} }
}
function handleAdvance() { function handleAdvance() {
const next = questionBox.next(); const next = questionBox.next();
if (next.done) handleFinish(); if (next.done) handleFinish();
@ -69,6 +68,7 @@ function GameCore<T>({ questions, Display, timeLimit, Instructions, studyLink, i
}).catch(console.error); }).catch(console.error);
} }
function handleFinish() { function handleFinish() {
onStartStop("stop");
setFinish("pass"); setFinish("pass");
rewardRef.current?.rewardMe(); rewardRef.current?.rewardMe();
if (!user) return; if (!user) return;
@ -80,20 +80,23 @@ function GameCore<T>({ questions, Display, timeLimit, Instructions, studyLink, i
handleResult(result); handleResult(result);
} }
function handleQuit() { function handleQuit() {
setFinish(null); onStartStop("stop");
setFinish(undefined);
setCurrent(undefined); setCurrent(undefined);
} }
function handleRestart() { function handleRestart() {
onStartStop("start");
const newQuestionBox = questions(); const newQuestionBox = questions();
const { value } = newQuestionBox.next(); const { value } = newQuestionBox.next();
// just for type safety -- the generator will have at least one question // just for type safety -- the generator will have at least one question
if (!value) return; if (!value) return;
setQuestionBox(newQuestionBox); setQuestionBox(newQuestionBox);
setFinish(null); setFinish(undefined);
setCurrent(value); setCurrent(value);
setTimerKey(prev => prev + 1); setTimerKey(prev => prev + 1);
} }
function handleTimeOut() { function handleTimeOut() {
onStartStop("stop");
setFinish("time out"); setFinish("time out");
navigator.vibrate(errorVibration); navigator.vibrate(errorVibration);
} }
@ -107,7 +110,7 @@ function GameCore<T>({ questions, Display, timeLimit, Instructions, studyLink, i
} }
const progressColor = finish === "pass" const progressColor = finish === "pass"
? "success" ? "success"
: finish === "fail" : typeof finish === "object"
? "danger" ? "danger"
: "primary"; : "primary";
return <div> return <div>
@ -130,7 +133,7 @@ function GameCore<T>({ questions, Display, timeLimit, Instructions, studyLink, i
</div>} </div>}
<Reward ref={rewardRef} config={{ lifetime: 130, spread: 90, elementCount: 125 }} type="confetti"> <Reward ref={rewardRef} config={{ lifetime: 130, spread: 90, elementCount: 125 }} type="confetti">
<div className="py-3"> <div className="py-3">
{finish === null && {finish === undefined &&
(current (current
? <div> ? <div>
<Display question={current.question} callback={handleCallback} /> <Display question={current.question} callback={handleCallback} />
@ -151,8 +154,12 @@ function GameCore<T>({ questions, Display, timeLimit, Instructions, studyLink, i
</h4> </h4>
<button className="btn btn-secondary mt-4" onClick={handleRestart}>Try Again</button> <button className="btn btn-secondary mt-4" onClick={handleRestart}>Try Again</button>
</div>} </div>}
{(finish === "fail" || finish === "time out") && <div> {(typeof finish === "object" || finish === "time out") && <div>
<h4 className="mt-4">{failMessage(current?.progress, finish)}</h4> <h4 className="mt-4">{failMessage(current?.progress, finish)}</h4>
{typeof finish === "object" && <div>
<div>The correct answer was:</div>
{finish?.answer}
</div>}
<div> <div>
<button className="btn btn-secondary my-4" onClick={handleRestart}>Try Again</button> <button className="btn btn-secondary my-4" onClick={handleRestart}>Try Again</button>
</div> </div>
@ -168,7 +175,7 @@ function GameCore<T>({ questions, Display, timeLimit, Instructions, studyLink, i
</div>; </div>;
} }
function failMessage(progress: Progress | undefined, finish: "time out" | "fail"): string { function failMessage(progress: Progress | undefined, finish: "time out" | { msg: "fail", answer: JSX.Element }): string {
const pDone = progress ? getPercentageDone(progress) : 0; const pDone = progress ? getPercentageDone(progress) : 0;
const { message, face } = pDone < 20 const { message, face } = pDone < 20
? { message: "No, sorry", face: "😑" } ? { message: "No, sorry", face: "😑" }
@ -179,7 +186,7 @@ function failMessage(progress: Progress | undefined, finish: "time out" | "fail"
: 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 finish === "fail" return typeof finish === "object"
? `${message} ${face}` ? `${message} ${face}`
: `⏳ Time's Up ${face}`; : `⏳ Time's Up ${face}`;
} }

View File

@ -1,12 +1,33 @@
import { useUser } from "../user-context"; import { useUser } from "../user-context";
import { useState } from "react";
function GameDisplay({ record: { title, Game, id } }: { record: GameRecord }) { function GameDisplay({ record: { title, Game, id } }: { record: GameRecord }) {
const { user } = useUser(); const { user } = useUser();
const [running, setRunning] = useState<boolean>(false);
const completed = user?.tests.some((t) => ( const completed = user?.tests.some((t) => (
// TODO: Or if it's in the locally stored (unposted test results) // TODO: Or if it's in the locally stored (unposted test results)
(t.done === true) && (t.id === id) (t.done === true) && (t.id === id)
)); ));
return <div> function onStartStop(a: "start" | "stop") {
if (a === "start" && !running) {
setRunning(true);
}
if (a === "stop") {
setRunning(false);
}
}
return <>
{running && <div style={{
position: "absolute",
backgroundColor: "rgba(255, 255, 255, 0.3)",
backdropFilter: "blur(7px)",
top: "0px",
left: "0px",
width: "100%",
height: "100%",
zIndex: 2,
}}></div>}
<div style={{ position: "relative", zIndex: 9999 }}>
<div className="d-flex flex-row justify-content-between align-items-center"> <div className="d-flex flex-row justify-content-between align-items-center">
<div> <div>
<h4 className="my-4"><span role="img" aria-label="">🎮</span> {title}</h4> <h4 className="my-4"><span role="img" aria-label="">🎮</span> {title}</h4>
@ -15,8 +36,9 @@ function GameDisplay({ record: { title, Game, id } }: { record: GameRecord }) {
<h4>{completed ? "✅" : ""}</h4> <h4>{completed ? "✅" : ""}</h4>
</div> </div>
</div> </div>
<Game /> {Game(onStartStop)}
</div> </div>
</>
} }
export default GameDisplay; export default GameDisplay;

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import { useState } from "react";
import games from "./games"; import games from "./games";
import { useUser } from "../user-context"; import { useUser } from "../user-context";
import Link from "../components/Link"; import Link from "../components/Link";
@ -37,7 +37,7 @@ function GamesBrowser() {
</div> </div>
</div> </div>
<SmoothCollapse expanded={open}> <SmoothCollapse expanded={open}>
<Game /> {Game(() => null)}
</SmoothCollapse> </SmoothCollapse>
</div> </div>
})} })}

View File

@ -1,3 +1,4 @@
import EquativeGame from "./sub-cores/EquativeGame";
import GenderGame from "./sub-cores/GenderGame"; import GenderGame from "./sub-cores/GenderGame";
import UnisexNounGame from "./sub-cores/UnisexNounGame"; import UnisexNounGame from "./sub-cores/UnisexNounGame";
@ -5,7 +6,7 @@ function makeGameRecord(
title: string, title: string,
id: string, id: string,
studyLink: string, studyLink: string,
game: (id: string, link: string) => (() => JSX.Element), game: (id: string, link: string) => ((s: (a: "start" | "stop") => void) => JSX.Element),
): GameRecord { ): GameRecord {
return { return {
title, title,
@ -19,19 +20,68 @@ export const nounGenderGame1 = makeGameRecord(
"Identify Noun Genders - Level 1", "Identify Noun Genders - Level 1",
"gender-nouns-1", "gender-nouns-1",
"/nouns/nouns-gender#gender-by-ending", "/nouns/nouns-gender#gender-by-ending",
(id, link) => () => <GenderGame id={id} level={1} link={link} />, (id, link) => (s: (a: "start" | "stop") => void) => <GenderGame id={id} level={1} link={link} onStartStop={s} />,
); );
export const nounGenderGame2 = makeGameRecord( export const nounGenderGame2 = makeGameRecord(
"Identify Noun Genders - Level 2", "Identify Noun Genders - Level 2",
"gender-nouns-2", "gender-nouns-2",
"/nouns/nouns-gender#exceptions", "/nouns/nouns-gender#exceptions",
(id, link) => () => <GenderGame id={id} level={2} link={link} />, (id, link) => (s: (a: "start" | "stop") => void) => <GenderGame id={id} level={2} link={link} onStartStop={s} />,
); );
export const unisexNounGame = makeGameRecord( export const unisexNounGame = makeGameRecord(
"Changing genders on unisex nouns", "Changing genders on unisex nouns",
"unisex-nouns-1", "unisex-nouns-1",
"/nouns/nouns-unisex/", "/nouns/nouns-unisex/",
(id, link) => () => <UnisexNounGame id={id} link={link} />, (id, link) => (s: (a: "start" | "stop") => void) => <UnisexNounGame id={id} link={link} onStartStop={s} />,
);
export const equativeGamePresent = makeGameRecord(
"Write the present equative",
"equative-present",
"/equatives/present-equative/",
(id, link) => (s: (a: "start" | "stop") => void) => <EquativeGame id={id} link={link} tense="present" onStartStop={s} />,
);
export const equativeGameHabitual = makeGameRecord(
"Write the habitual equative",
"equative-habitual",
"/equatives/habitual-equative/",
(id, link) => (s: (a: "start" | "stop") => void) => <EquativeGame id={id} link={link} tense="habitual" onStartStop={s} />,
);
export const equativeGameSubjunctive = makeGameRecord(
"Write the subjunctive equative",
"equative-subjunctive",
"/equatives/other-equatives/#subjunctive-equative",
(id, link) => (s: (a: "start" | "stop") => void) => <EquativeGame id={id} link={link} tense="subjunctive" onStartStop={s} />,
);
export const equativeGameFuture = makeGameRecord(
"Write the future equative",
"equative-future",
"/equatives/other-equatives/#future-equative",
(id, link) => (s: (a: "start" | "stop") => void) => <EquativeGame id={id} link={link} tense="future" onStartStop={s} />,
);
export const equativeGamePast = makeGameRecord(
"Write the past equative",
"equative-past",
"/equatives/other-equatives/#past-equative",
(id, link) => (s: (a: "start" | "stop") => void) => <EquativeGame id={id} link={link} tense="past" onStartStop={s} />,
);
export const equativeGameWouldBe = makeGameRecord(
'Write the "would be" equative',
"equative-would-be",
"equatives/other-equatives/#would-be-equative",
(id, link) => (s: (a: "start" | "stop") => void) => <EquativeGame id={id} link={link} tense="wouldBe" onStartStop={s} />,
);
export const equativeGamePastSubjunctive = makeGameRecord(
'Write the past subjunctive equative',
"equative-past-subjunctive",
"/equatives/other-equatives/#past-subjunctive",
(id, link) => (s: (a: "start" | "stop") => void) => <EquativeGame id={id} link={link} tense="pastSubjunctive" onStartStop={s} />,
); );
const games: { chapter: string, items: GameRecord[] }[] = [ const games: { chapter: string, items: GameRecord[] }[] = [
@ -43,6 +93,18 @@ const games: { chapter: string, items: GameRecord[] }[] = [
unisexNounGame, unisexNounGame,
], ],
}, },
{
chapter: "Equatives",
items: [
equativeGamePresent,
equativeGameHabitual,
equativeGameSubjunctive,
equativeGameFuture,
equativeGamePast,
equativeGameWouldBe,
equativeGamePastSubjunctive,
],
},
]; ];
export default games; export default games;

View File

@ -0,0 +1,265 @@
import { useState } from "react";
import {
makeProgress,
} from "../../lib/game-utils";
import GameCore from "../GameCore";
import {
Types as T,
Examples,
defaultTextOptions as opts,
standardizePashto,
typePredicates as tp,
makeNounSelection,
randFromArray,
renderEP,
compileEP,
flattenLengths,
randomPerson,
InlinePs,
grammarUnits,
} from "@lingdocs/pashto-inflector";
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"},
].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."},
].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 amount = 20;
const timeLimit = 55;
type Question = {
phrase: { ps: T.PsString, e?: string[] },
equative: T.EquativeRendered,
};
const pronounTypes = [
[T.Person.FirstSingMale, T.Person.FirstSingFemale],
[T.Person.SecondSingMale, T.Person.SecondSingFemale],
[T.Person.ThirdSingMale],
[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({ id, link, tense, onStartStop }: { id: string, link: string, tense: T.EquativeTense, onStartStop: (a: "start" | "stop") => void }) {
function* questions (): Generator<Current<Question>> {
let pool = [...pronounTypes];
function makeRandPronoun(): T.PronounSelection {
let person: T.Person;
console.log(pool);
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,
};
}
for (let i = 0; i < amount; i++) {
console.log("one factory call");
const subj = randFromArray([
makeRandPronoun,
makeRandPronoun,
makeRandomNoun,
makeRandPronoun,
])();
const pred = randFromArray([...adjectives, ...locAdverbs]);
const EPS = makeEPS(subj, pred, tense);
const rendered = renderEP(EPS);
const compiled = compileEP(rendered, true, { equative: true, ba: false, kidsSection: true });
const phrase = {
ps: compiled.ps[0],
e: compiled.e,
};
yield {
progress: makeProgress(i, amount),
question: {
phrase,
equative: rendered.equative,
},
};
};
}
function Display({ question, callback }: QuestionDisplayProps<Question>) {
const [answer, setAnswer] = useState<string>("");
const [withBa, setWithBa] = useState<boolean>(false);
const handleInput = ({ target: { value }}: React.ChangeEvent<HTMLInputElement>) => {
setAnswer(value);
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const given = standardizePashto(answer.trim());
const correct = checkAnswer(given, question.equative.ps)
&& withBa === question.equative.hasBa;
if (correct) {
setAnswer("");
}
callback(!correct
? <div>
<div className="my-2">
{flattenLengths(question.equative.ps).reduce(((accum, curr, i): JSX.Element[] => (
[
...accum,
...i > 0 ? [<span className="text-muted"> or </span>] : [],
<span>{curr.p}</span>,
]
)), [] as JSX.Element[])}
</div>
<div><strong>{question.equative.hasBa ? "with" : "without"}</strong> a <InlinePs opts={opts}>{grammarUnits.baParticle}</InlinePs> in the kids' section.</div>
</div>
: true);
}
return <div>
<div className="pt-2 pb-1 mb-2" style={{ maxWidth: "300px", margin: "0 auto" }}>
{/*
@ts-ignore */}
<Examples opts={opts} ex={modExs([question.phrase.ps])}></Examples>
{question.phrase.e && question.phrase.e.map(e => (
<div className="text-muted mb-1">{e}</div>
))}
</div>
<form onSubmit={handleSubmit}>
<div className="my-3" style={{ maxWidth: "200px", margin: "0 auto" }}>
<input
type="text"
className="form-control"
autoComplete="off"
autoCapitalize="off"
spellCheck="false"
dir="auto"
value={answer}
onChange={handleInput}
/>
</div>
<div>
<div className="form-check mt-1">
<input
className="form-check-input"
type="checkbox"
checked={withBa}
onChange={e => setWithBa(e.target.checked)}
/>
<label className="form-check-label text-muted" htmlFor="OSVCheckbox">
with <InlinePs opts={opts}>{grammarUnits.baParticle}</InlinePs> in the <span style={{ color: kidsColor }}>kids' section</span>
</label>
</div>
<div className="text-center my-2">
{/* <div> */}
<button className="btn btn-primary" type="submit">check</button>
{/* </div> */}
{/* <div className="text-muted small text-center mt-2">
Type <kbd>Enter</kbd> to check
</div> */}
</div>
</div>
</form>
</div>
}
function Instructions() {
return <div>
<p className="lead">Fill in the blank with the correct <strong>{humanReadableTense(tense)} equative</strong> <strong>in Pashto script</strong></p>
</div>
}
return <GameCore
onStartStop={onStartStop}
studyLink={link}
questions={questions}
id={id}
Display={Display}
timeLimit={timeLimit}
Instructions={Instructions}
/>
};
function modExs(exs: T.PsString[]): { p: JSX.Element, f: JSX.Element }[] {
return exs.map(ps => {
if (!ps.p.includes(" ___ ")) {
return {
p: <>{ps.p}</>,
f: <>{ps.f}</>,
};
}
const splitP = ps.p.split(" ___ ");
const splitF = ps.f.split(" ___ ");
return {
p: <>{splitP[0]} <span style={{ color: kidsColor }}>___</span> {splitP[1]}</>,
f: <>{splitF[0]} <span style={{ color: kidsColor }}>___</span> {splitF[1]}</>,
};
});
}
function humanReadableTense(tense: T.EquativeTense): string {
return tense === "pastSubjunctive"
? "past subjunctive"
: tense === "wouldBe"
? `"would be"`
: tense;
}
function checkAnswer(given: string, answer: T.SingleOrLengthOpts<T.PsString[]>): boolean {
const possible = flattenLengths(answer);
return possible.some(x => given === x.p);
}
function makeEPS(subject: T.NPSelection, predicate: T.AdjectiveEntry | T.LocativeAdverbEntry, tense: T.EquativeTense): T.EPSelectionComplete {
return {
subject,
predicate: {
type: "Complement",
selection: tp.isAdjectiveEntry(predicate) ? {
type: "adjective",
entry: predicate,
} : {
type: "loc. adv.",
entry: predicate,
},
},
equative: {
tense,
negative: false,
},
omitSubject: false,
};
}

View File

@ -1,5 +1,4 @@
import { import {
getRandomFromList,
makeProgress, makeProgress,
} from "../../lib/game-utils"; } from "../../lib/game-utils";
import genderColors from "../../lib/gender-colors"; import genderColors from "../../lib/gender-colors";
@ -14,6 +13,7 @@ import {
isUnisexSet, isUnisexSet,
typePredicates as tp, typePredicates as tp,
firstVariation, firstVariation,
randFromArray,
} from "@lingdocs/pashto-inflector"; } from "@lingdocs/pashto-inflector";
import { nouns } from "../../words/words"; import { nouns } from "../../words/words";
import { categorize } from "../../lib/categorize"; import { categorize } from "../../lib/categorize";
@ -86,7 +86,7 @@ const exceptions: Record<string, CategorySet> = {
const amount = 35; const amount = 35;
export default function GenderGame({level, id, link}: { level: 1 | 2, id: string, link: string }) { export default function GenderGame({level, id, link, onStartStop }: { level: 1 | 2, id: string, link: string, onStartStop: (a: "start" | "stop") => void }) {
function* questions () { function* questions () {
const wordPool = {...types}; const wordPool = {...types};
const exceptionsPool = {...exceptions}; const exceptionsPool = {...exceptions};
@ -94,13 +94,13 @@ export default function GenderGame({level, id, link}: { level: 1 | 2, id: string
for (let i = 0; i < amount; i++) { for (let i = 0; i < amount; i++) {
const base = level === 1 const base = level === 1
? wordPool ? wordPool
: getRandomFromList([wordPool, exceptionsPool]); : randFromArray([wordPool, exceptionsPool]);
const gender = getRandomFromList(genders); const gender = randFromArray(genders);
let typeToUse: string; let typeToUse: string;
do { do {
typeToUse = getRandomFromList(Object.keys(base[gender])); typeToUse = randFromArray(Object.keys(base[gender]));
} while (!base[gender][typeToUse].length); } while (!base[gender][typeToUse].length);
const question = getRandomFromList(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 { yield {
progress: makeProgress(i, amount), progress: makeProgress(i, amount),
@ -111,7 +111,10 @@ export default function GenderGame({level, id, link}: { level: 1 | 2, id: string
function Display({ question, callback }: QuestionDisplayProps<T.DictionaryEntry>) { function Display({ question, callback }: QuestionDisplayProps<T.DictionaryEntry>) {
function check(gender: "m" | "f") { function check(gender: "m" | "f") {
callback(!nounNotIn(gender === "m" ? mascNouns : femNouns)(question)); const correct = !nounNotIn(gender === "m" ? mascNouns : femNouns)(question);
callback(!correct
? <div>ANSWER HERE</div>
: true);
} }
return <div> return <div>
<div className="mb-4" style={{ fontSize: "larger" }}> <div className="mb-4" style={{ fontSize: "larger" }}>
@ -132,12 +135,13 @@ export default function GenderGame({level, id, link}: { level: 1 | 2, id: string
function Instructions() { function Instructions() {
return <div> return <div>
<h4>Choose the right gender for each word</h4> <h5>Choose the right gender for each word</h5>
{level === 2 && <div> Exceptions included...</div>} {level === 2 && <div> Exceptions included...</div>}
</div> </div>
} }
return <GameCore return <GameCore
onStartStop={onStartStop}
studyLink={link} studyLink={link}
questions={questions} questions={questions}
id={id} id={id}

View File

@ -1,6 +1,5 @@
import React, { useState } from "react"; import { useState } from "react";
import { import {
getRandomFromList,
makeProgress, makeProgress,
compareF, compareF,
} from "../../lib/game-utils"; } from "../../lib/game-utils";
@ -14,6 +13,7 @@ import {
standardizePashto, standardizePashto,
firstVariation, firstVariation,
typePredicates as tp, typePredicates as tp,
randFromArray,
} from "@lingdocs/pashto-inflector"; } from "@lingdocs/pashto-inflector";
import { nouns } from "../../words/words"; import { nouns } from "../../words/words";
import { intoPatterns } from "../../lib/categorize"; import { intoPatterns } from "../../lib/categorize";
@ -28,20 +28,20 @@ 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 }: { id: string, link: string }) { export default function UnisexNounGame({ id, link, onStartStop }: { id: string, link: string, onStartStop: (a: "start" | "stop") => void }) {
function* questions (): Generator<Current<Question>> { function* questions (): Generator<Current<Question>> {
let pool = { ...types }; let pool = { ...types };
for (let i = 0; i < amount; i++) { for (let i = 0; i < amount; i++) {
const keys = Object.keys(types) as NType[]; const keys = Object.keys(types) as NType[];
let type: NType let type: NType
do { do {
type = getRandomFromList(keys); type = randFromArray(keys);
} while (!pool[type].length); } while (!pool[type].length);
const entry = getRandomFromList<T.UnisexNounEntry>( const entry = randFromArray<T.UnisexNounEntry>(
// @ts-ignore // @ts-ignore
pool[type] pool[type]
); );
const gender = getRandomFromList(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 { yield {
@ -84,7 +84,9 @@ export default function UnisexNounGame({ id, link }: { id: string, link: string
if (correct) { if (correct) {
setAnswer(""); setAnswer("");
} }
callback(correct); callback(!correct
? <div>CORRECT ANSWER HERE</div>
: true);
} }
return <div> return <div>
@ -120,11 +122,12 @@ export default function UnisexNounGame({ id, link }: { id: string, link: string
function Instructions() { function Instructions() {
return <div> return <div>
<h4>Change the gender of a given noun</h4> <h5>Change the gender of a given noun</h5>
</div> </div>
} }
return <GameCore return <GameCore
onStartStop={onStartStop}
studyLink={link} studyLink={link}
questions={questions} questions={questions}
id={id} id={id}

View File

@ -26,10 +26,6 @@ export function getPercentageDone(progress: Progress): number {
); );
} }
export function getRandomFromList<T>(list: T[]): T {
return list[Math.floor((Math.random()*list.length))];
}
export function makeProgress(i: number, total: number): Progress { export function makeProgress(i: number, total: number): Progress {
return { current: i + 1, total }; return { current: i + 1, total };
} }

View File

@ -12,12 +12,12 @@ type QuestionGenerator<T> = Generator<Current<T>, void, unknown>;
type QuestionDisplayProps<T> = { type QuestionDisplayProps<T> = {
question: T, question: T,
callback: (correct: boolean) => void, callback: (correct: true | JSX.Element) => void,
}; };
type GameRecord = { type GameRecord = {
title: string, title: string,
id: string, id: string,
studyLink: string, studyLink: string,
Game: () => JSX.Element, Game: (onStartStop: (a: "start" | "stop") => void) => JSX.Element,
}; };

View File

@ -1684,10 +1684,10 @@
pbf "^3.2.1" pbf "^3.2.1"
rambda "^6.7.0" rambda "^6.7.0"
"@lingdocs/pashto-inflector@^2.4.2": "@lingdocs/pashto-inflector@^2.4.9":
version "2.4.2" version "2.5.0"
resolved "https://npm.lingdocs.com/@lingdocs%2fpashto-inflector/-/pashto-inflector-2.4.2.tgz#584be92d04051a4b3f4e557fa4473671570ed24d" resolved "https://npm.lingdocs.com/@lingdocs%2fpashto-inflector/-/pashto-inflector-2.5.0.tgz#4f90a1d2db15e8389cd84f0a576c37b814f303fd"
integrity sha512-+vjvNOIgVW74+NQMdU2ctAK/ISL37WPiZItpt43izvzNJ85/mREmArzTzMCxbN+kFtYpEwAFe5tRsIJgviOBQQ== integrity sha512-RwlBQNwNsKxvICHjx6MwOwetzGQWLdrwjWOBLMfSqbRqQzNWQrjm/Gp4+TfIT89IUtutxVSxLXlgwgUq6ebKpA==
dependencies: dependencies:
classnames "^2.2.6" classnames "^2.2.6"
jsurl2 "^2.1.0" jsurl2 "^2.1.0"