nice good refactoring to make the practice and test modes work properly
This commit is contained in:
parent
5d8888634f
commit
07ea0a286a
|
@ -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",
|
||||||
|
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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[] => (
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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
|
||||||
*
|
*
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue