Compare commits
6 Commits
acafc4fe79
...
471cbef217
Author | SHA1 | Date |
---|---|---|
adueck | 471cbef217 | |
adueck | 7693ee10b3 | |
adueck | 7b933effa4 | |
adueck | 9c9fa06588 | |
adueck | b03e418783 | |
adueck | 290c453317 |
|
@ -17,7 +17,7 @@ export default function MinimalPairs({
|
|||
return (
|
||||
<div>
|
||||
<h5 className="my-3" onClick={() => setOpened((x) => !x)}>
|
||||
{opened ? "▼" : "▶"} View Pairs
|
||||
{opened ? "▼" : "▶"} Browse Pairs
|
||||
</h5>
|
||||
<SmoothCollapse expanded={opened}>
|
||||
{section.pairs.map((pairs, i) => (
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
const minimalPairsSection = [
|
||||
"t and T",
|
||||
"d and D",
|
||||
"r and R",
|
||||
"n and N",
|
||||
"a and aa",
|
||||
"ay and uy",
|
||||
"ay and e",
|
||||
"ee and e",
|
||||
] as const;
|
||||
export type MinimalPairsSection = (typeof minimalPairsSection)[number];
|
|
@ -11,12 +11,23 @@ import psmd from "../../lib/psmd";
|
|||
import Link from "../../components/Link";
|
||||
import MinimalPairs from "./MinimalPairs.tsx";
|
||||
import minimalPairs from "./minimal-pairs.ts";
|
||||
import {
|
||||
minimalPairsT,
|
||||
minimalPairsD,
|
||||
minimalPairsR,
|
||||
minimalPairsN,
|
||||
minimalPairsAa,
|
||||
minimalPairsAyUy,
|
||||
minimalPairsAyE,
|
||||
minimalPairsEeE,
|
||||
} from "../../games/games";
|
||||
import GameDisplay from "../../games/GameDisplay";
|
||||
|
||||
There are certain sounds in Pashto that are quite difficult for some learners to distinguish.
|
||||
|
||||
For example, English speakers have a very hard time hearing the difference between the dental <InlinePs opts={opts} ps={{ p: "ت", f: "t" }}/> and retroflex <InlinePs opts={opts} ps={{ p: "ټ", f: "T" }}/>. Some of the vowels like <InlinePs opts={opts} ps={{ p: "ي", f: "ee" }}/> and <InlinePs opts={opts} ps={{ p: "ې", f: "e" }}/> can also be very tricky to distinguish.
|
||||
|
||||
Here are some examples of words that vary by these different sounds. Listen to them to train your ear to the difference, and then use the games to see if you can hear the difference yourself. (Games coming soon! 🚧)
|
||||
Here are some examples of words that vary by these different sounds. Listen to them to train your ear to the difference, and then use the games to see if you can hear the difference yourself.
|
||||
|
||||
## ت - t and ټ - T
|
||||
|
||||
|
@ -25,6 +36,8 @@ Here are some examples of words that vary by these different sounds. Listen to t
|
|||
section={minimalPairs.find((x) => x.title === "t and T")}
|
||||
/>
|
||||
|
||||
<GameDisplay record={minimalPairsT} />
|
||||
|
||||
## د - d and ډ - D
|
||||
|
||||
<MinimalPairs
|
||||
|
@ -32,6 +45,8 @@ Here are some examples of words that vary by these different sounds. Listen to t
|
|||
section={minimalPairs.find((x) => x.title === "d and D")}
|
||||
/>
|
||||
|
||||
<GameDisplay record={minimalPairsD} />
|
||||
|
||||
## ر - r and ړ - R
|
||||
|
||||
<MinimalPairs
|
||||
|
@ -39,6 +54,8 @@ Here are some examples of words that vary by these different sounds. Listen to t
|
|||
section={minimalPairs.find((x) => x.title === "r and R")}
|
||||
/>
|
||||
|
||||
<GameDisplay record={minimalPairsR} />
|
||||
|
||||
## ن - n and ڼ - N
|
||||
|
||||
<MinimalPairs
|
||||
|
@ -46,6 +63,8 @@ Here are some examples of words that vary by these different sounds. Listen to t
|
|||
section={minimalPairs.find((x) => x.title === "n and N")}
|
||||
/>
|
||||
|
||||
<GameDisplay record={minimalPairsN} />
|
||||
|
||||
## ه - a and ا - aa
|
||||
|
||||
<MinimalPairs
|
||||
|
@ -53,6 +72,8 @@ Here are some examples of words that vary by these different sounds. Listen to t
|
|||
section={minimalPairs.find((x) => x.title === "a and aa")}
|
||||
/>
|
||||
|
||||
<GameDisplay record={minimalPairsAa} />
|
||||
|
||||
## ی - ay and ۍ - uy
|
||||
|
||||
<MinimalPairs
|
||||
|
@ -60,6 +81,8 @@ Here are some examples of words that vary by these different sounds. Listen to t
|
|||
section={minimalPairs.find((x) => x.title === "ay and uy")}
|
||||
/>
|
||||
|
||||
<GameDisplay record={minimalPairsAyUy} />
|
||||
|
||||
## ی - ay and ې - e
|
||||
|
||||
<MinimalPairs
|
||||
|
@ -67,9 +90,13 @@ Here are some examples of words that vary by these different sounds. Listen to t
|
|||
section={minimalPairs.find((x) => x.title === "ay and e")}
|
||||
/>
|
||||
|
||||
<GameDisplay record={minimalPairsAyE} />
|
||||
|
||||
## ي - ee and ې - e
|
||||
|
||||
<MinimalPairs
|
||||
opts={opts}
|
||||
section={minimalPairs.find((x) => x.title === "ee and e")}
|
||||
/>
|
||||
|
||||
<GameDisplay record={minimalPairsEeE} />
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,4 @@
|
|||
import { useState, useRef, useEffect } from "react";
|
||||
import { useState, useRef, useEffect, memo } from "react";
|
||||
import { CountdownCircleTimer } from "react-countdown-circle-timer";
|
||||
import Reward, { RewardElement } from "react-rewards";
|
||||
import Link from "../components/Link";
|
||||
|
@ -25,7 +25,7 @@ type GameState<Question> = (
|
|||
}
|
||||
) & {
|
||||
numberComplete: number;
|
||||
current: Question;
|
||||
current: Question | undefined;
|
||||
timerKey: number;
|
||||
strikes: number;
|
||||
justStruck: boolean;
|
||||
|
@ -74,10 +74,13 @@ function GameCore<Question>({
|
|||
timeLimit: number;
|
||||
amount: number;
|
||||
}) {
|
||||
// TODO STOP THE DOUBLE POOL DIPPING !!!
|
||||
// POSSIBLE SOLUTION - allow question to be undefined... then use useEffect
|
||||
// to grab the first question
|
||||
const initialState: GameState<Question> = {
|
||||
mode: "intro",
|
||||
numberComplete: 0,
|
||||
current: getQuestion(),
|
||||
current: undefined,
|
||||
timerKey: 0,
|
||||
strikes: 0,
|
||||
justStruck: false,
|
||||
|
@ -91,6 +94,10 @@ function GameCore<Question>({
|
|||
useState<GameState<Question>>(initialState);
|
||||
useEffect(() => {
|
||||
parent.current && autoAnimate(parent.current);
|
||||
setStateDangerous((s) => ({
|
||||
...s,
|
||||
current: getQuestion(),
|
||||
}));
|
||||
}, [parent]);
|
||||
|
||||
const gameReducer = (
|
||||
|
@ -173,6 +180,7 @@ function GameCore<Question>({
|
|||
if (action.type === "quit") {
|
||||
return {
|
||||
...initialState,
|
||||
current: getQuestion(),
|
||||
timerKey: gs.timerKey + 1,
|
||||
};
|
||||
}
|
||||
|
@ -372,7 +380,7 @@ function GameCore<Question>({
|
|||
<ActionButtons />
|
||||
</div>
|
||||
)}
|
||||
{gameRunning && (
|
||||
{gameRunning && state.current && (
|
||||
<Display
|
||||
question={state.current}
|
||||
callback={(correct) =>
|
||||
|
@ -394,7 +402,7 @@ function GameCore<Question>({
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
{state.showAnswer && state.mode === "practice" && (
|
||||
{state.showAnswer && state.mode === "practice" && state.current && (
|
||||
<div className="my-2">
|
||||
<div className="my-1">
|
||||
<DisplayCorrectAnswer question={state.current} />
|
||||
|
@ -423,7 +431,8 @@ function GameCore<Question>({
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
{(state.mode === "timeout" || state.mode === "fail") && (
|
||||
{(state.mode === "timeout" || state.mode === "fail") &&
|
||||
state.current && (
|
||||
<div className="mb-4">
|
||||
<h4 className="mt-4">
|
||||
{failMessage({
|
||||
|
|
|
@ -3,6 +3,7 @@ import VerbGame from "./sub-cores/VerbGame";
|
|||
import GenderGame from "./sub-cores/GenderGame";
|
||||
import PluralNounGame from "./sub-cores/PluralNounGame";
|
||||
import UnisexNounGame from "./sub-cores/UnisexNounGame";
|
||||
import MinimalPairsGame from "./sub-cores/MinimalPairsGame";
|
||||
import EquativeSituations from "./sub-cores/EquativeSituations";
|
||||
import VerbSituations from "./sub-cores/VerbSituations";
|
||||
import EquativeIdentify from "./sub-cores/EquativeIdentify";
|
||||
|
@ -13,6 +14,64 @@ import PerfectVerbsIntransitive from "./sub-cores/PerfectGame";
|
|||
import NPAdjWriting from "./sub-cores/NPAdjGame";
|
||||
import EPAdjGame from "./sub-cores/EPAdjGame";
|
||||
|
||||
// MINIMAL PAIRS
|
||||
export const minimalPairsT = makeGameRecord({
|
||||
title: "Minimal Pairs - t and T",
|
||||
id: "minimal-pairs-t",
|
||||
link: "/writing/minimal-pairs/#ت---t-and-ټ---t",
|
||||
level: "t and T",
|
||||
SubCore: MinimalPairsGame,
|
||||
});
|
||||
export const minimalPairsD = makeGameRecord({
|
||||
title: "Minimal Pairs - d and D",
|
||||
id: "minimal-pairs-d",
|
||||
link: "/writing/minimal-pairs/#د---d-and-ډ---d",
|
||||
level: "d and D",
|
||||
SubCore: MinimalPairsGame,
|
||||
});
|
||||
export const minimalPairsR = makeGameRecord({
|
||||
title: "Minimal Pairs - r and R",
|
||||
id: "minimal-pairs-r",
|
||||
link: "/writing/minimal-pairs/#ر---r-and-ړ---r",
|
||||
level: "r and R",
|
||||
SubCore: MinimalPairsGame,
|
||||
});
|
||||
export const minimalPairsN = makeGameRecord({
|
||||
title: "Minimal Pairs - n and N",
|
||||
id: "minimal-pairs-n",
|
||||
link: "/writing/aiilmmn - pairs / #ن-- - n - and - ڼ-- - n",
|
||||
level: "n and N",
|
||||
SubCore: MinimalPairsGame,
|
||||
});
|
||||
export const minimalPairsAa = makeGameRecord({
|
||||
title: "Minimal Pairs - a and aa",
|
||||
id: "minimal-pairs-aa",
|
||||
link: "/writing/minimal-pairs/#ه---a-and-ا---aa",
|
||||
level: "a and aa",
|
||||
SubCore: MinimalPairsGame,
|
||||
});
|
||||
export const minimalPairsAyUy = makeGameRecord({
|
||||
title: "Minimal Pairs - ay and uy",
|
||||
id: "minimal-pairs-ay-uy",
|
||||
link: "/writing/minimal-pairs/#ی---ay-and-ۍ---uy",
|
||||
level: "ay and uy",
|
||||
SubCore: MinimalPairsGame,
|
||||
});
|
||||
export const minimalPairsAyE = makeGameRecord({
|
||||
title: "Minimal Pairs - ay and e",
|
||||
id: "minimal-pairs-ay-e",
|
||||
link: "/writing/minimal-pairs/#ی---ay-and-ې---e",
|
||||
level: "ay and e",
|
||||
SubCore: MinimalPairsGame,
|
||||
});
|
||||
export const minimalPairsEeE = makeGameRecord({
|
||||
title: "Minimal Pairs - ee and e",
|
||||
id: "minimal-pairs-ee-e",
|
||||
link: "ي - ee and ې - e",
|
||||
level: "ee and e",
|
||||
SubCore: MinimalPairsGame,
|
||||
});
|
||||
|
||||
// NOUNS
|
||||
export const nounGenderGame1 = makeGameRecord({
|
||||
title: "Identify Noun Genders - Level 1",
|
||||
|
@ -443,6 +502,19 @@ export const npWithAdjectivesInSandwiches = makeGameRecord({
|
|||
});
|
||||
|
||||
const games: { chapter: string; items: GameRecord[] }[] = [
|
||||
{
|
||||
chapter: "Minimal Pairs",
|
||||
items: [
|
||||
minimalPairsT,
|
||||
minimalPairsD,
|
||||
minimalPairsR,
|
||||
minimalPairsN,
|
||||
minimalPairsAa,
|
||||
minimalPairsAyUy,
|
||||
minimalPairsAyE,
|
||||
minimalPairsEeE,
|
||||
],
|
||||
},
|
||||
{
|
||||
chapter: "Nouns",
|
||||
items: [nounGenderGame1, nounGenderGame2, unisexNounGame, pluralNounGame],
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
import GameCore from "../GameCore";
|
||||
import {
|
||||
Types as T,
|
||||
defaultTextOptions as opts,
|
||||
randFromArray,
|
||||
removeAccents,
|
||||
} from "@lingdocs/ps-react";
|
||||
import minimalPairs from "../../content/writing/minimal-pairs";
|
||||
import { makePool } from "../../lib/pool";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { MinimalPairsSection } from "../../content/writing/minimal-pairs-type";
|
||||
|
||||
// is it removing from the pool properly ? or is it a problem with strict mode doing double?
|
||||
|
||||
const amount = 20;
|
||||
type EntryWF = { f: string; entry: T.DictionaryEntry };
|
||||
type MinimalPair = [EntryWF, EntryWF];
|
||||
type Question = {
|
||||
pair: MinimalPair;
|
||||
selected: 0 | 1;
|
||||
};
|
||||
|
||||
export default function MinimalPairsGame({
|
||||
level,
|
||||
id,
|
||||
link,
|
||||
inChapter,
|
||||
}: {
|
||||
inChapter: boolean;
|
||||
level: MinimalPairsSection;
|
||||
id: string;
|
||||
link: string;
|
||||
}) {
|
||||
const getPair = makePool<MinimalPair>(
|
||||
// @ts-ignore
|
||||
minimalPairs.find((x) => x.title === level)?.pairs || []
|
||||
);
|
||||
function getQuestion(): Question {
|
||||
const pair = getPair();
|
||||
const selected: 0 | 1 = randFromArray([0, 1]);
|
||||
return { pair, selected };
|
||||
}
|
||||
function Display({ question, callback }: QuestionDisplayProps<Question>) {
|
||||
const audioRef = useRef<HTMLAudioElement>();
|
||||
useEffect(() => {
|
||||
if (audioRef && audioRef.current) {
|
||||
audioRef.current.play();
|
||||
}
|
||||
}, [question]);
|
||||
const selected = getSelected(question);
|
||||
const audioSrc = getAudioSrc(selected);
|
||||
function playAudio() {
|
||||
if (audioRef && audioRef.current) {
|
||||
audioRef.current.play();
|
||||
}
|
||||
}
|
||||
function check(guess: 0 | 1) {
|
||||
return () => callback(question.selected === guess);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<audio
|
||||
src={audioSrc}
|
||||
// @ts-expect-error // typing not playing nice here
|
||||
ref={audioRef}
|
||||
/>
|
||||
<div>
|
||||
<button className="btn btn-lg btn-primary mt-3" onClick={playAudio}>
|
||||
<i className="fas fa-play" />
|
||||
</button>
|
||||
</div>
|
||||
<PairButtons
|
||||
renderButton={(n) => (
|
||||
<WordButton
|
||||
entry={question.pair[n]}
|
||||
handleClick={check(n)}
|
||||
type="guess"
|
||||
hideAccents={true}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Instructions() {
|
||||
return (
|
||||
<div>
|
||||
<h5>Listen and identify the correct word in a minimal pair</h5>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DisplayCorrectAnswer({ question }: { question: Question }) {
|
||||
const audioRef0 = useRef<HTMLAudioElement>();
|
||||
const audioRef1 = useRef<HTMLAudioElement>();
|
||||
const [audioSrc0, audioSrc1] = question.pair.map(getAudioSrc);
|
||||
const playAudio = (n: 0 | 1) => () => {
|
||||
const audio = n === 0 ? audioRef0 : audioRef1;
|
||||
audio.current?.play();
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<audio
|
||||
src={audioSrc0}
|
||||
// @ts-expect-error // typing not playing nice here
|
||||
ref={audioRef0}
|
||||
preload="auto"
|
||||
/>
|
||||
<audio
|
||||
src={audioSrc1}
|
||||
// @ts-expect-error // typing not playing nice here
|
||||
ref={audioRef1}
|
||||
preload="auto"
|
||||
/>
|
||||
<PairButtons
|
||||
renderButton={(n) => (
|
||||
<WordButton
|
||||
entry={question.pair[n]}
|
||||
handleClick={playAudio(n)}
|
||||
key={`answer-${n}`}
|
||||
type={question.selected === n ? "correct" : "incorrect"}
|
||||
hideAccents={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GameCore
|
||||
inChapter={inChapter}
|
||||
studyLink={link}
|
||||
getQuestion={getQuestion}
|
||||
id={id}
|
||||
Display={Display}
|
||||
DisplayCorrectAnswer={DisplayCorrectAnswer}
|
||||
amount={amount}
|
||||
timeLimit={90}
|
||||
Instructions={Instructions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PairButtons({
|
||||
renderButton,
|
||||
}: {
|
||||
renderButton: (n: 0 | 1) => JSX.Element;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="mt-4 d-flex justify-content-center"
|
||||
style={{ margin: "0 auto", gap: "2.5rem" }}
|
||||
>
|
||||
{([0, 1] as const).map(renderButton)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WordButton({
|
||||
entry,
|
||||
handleClick,
|
||||
type,
|
||||
hideAccents,
|
||||
}: {
|
||||
entry: EntryWF;
|
||||
handleClick: () => void;
|
||||
type: "guess" | "correct" | "incorrect";
|
||||
hideAccents: boolean;
|
||||
}) {
|
||||
const btnColor =
|
||||
type === "guess" ? "light" : type === "correct" ? "success" : "danger";
|
||||
return (
|
||||
<button
|
||||
className={`btn btn-lg btn-${btnColor} mr-3`}
|
||||
style={{ minWidth: "8rem" }}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="d-flex justify-content-around">
|
||||
{type === "guess" ? null : (
|
||||
<div className="mr-3 d-flex flex-column justify-content-around">
|
||||
<i className={`fas fa-${type === "correct" ? "check" : "times"}`} />
|
||||
<i className="fas fa-play" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div>{hideAccents ? removeAccents(entry.f) : entry.f}</div>
|
||||
<div>{entry.entry.p}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function getAudioSrc(entry: EntryWF): string {
|
||||
const tag = entry.entry.a === 1 ? "" : "f";
|
||||
return `https://storage.lingdocs.com/audio/${entry.entry.ts}${tag}.mp3`;
|
||||
}
|
||||
|
||||
function getSelected(question: Question): {
|
||||
f: string;
|
||||
entry: T.DictionaryEntry;
|
||||
} {
|
||||
return question.pair[question.selected];
|
||||
}
|
|
@ -6,7 +6,7 @@ import equal from "fast-deep-equal";
|
|||
* @param poolBase an array of things you want to use as the pool to pick from
|
||||
* @param removalLaxity If set, thery will be a n% chance that the pick will NOT
|
||||
* be removed after use. Defaults to 0, meaning that every time an item is picked
|
||||
* it is removed from the. 100 means that items will never be removed from the pool.
|
||||
* it is removed from the pool. 100 means that items will never be removed from the pool.
|
||||
* @returns
|
||||
*/
|
||||
export function makePool<P>(poolBase: P[], removalLaxity = 0): () => P {
|
||||
|
|
Loading…
Reference in New Issue