Compare commits

..

6 Commits

Author SHA1 Message Date
adueck 471cbef217 m 2024-12-18 23:51:25 -05:00
adueck 7693ee10b3 fixed pool issue! 2024-12-18 23:47:12 -05:00
adueck 7b933effa4 debug 2024-12-18 23:17:09 -05:00
adueck 9c9fa06588 oops 2024-12-18 23:10:06 -05:00
adueck b03e418783 m 2024-12-18 21:29:27 -05:00
adueck 290c453317 minimal pairs game 2024-12-18 21:06:16 -05:00
8 changed files with 471 additions and 252 deletions

View File

@ -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) => (

View File

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

View File

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

View File

@ -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,24 +431,25 @@ function GameCore<Question>({
</button>
</div>
)}
{(state.mode === "timeout" || state.mode === "fail") && (
<div className="mb-4">
<h4 className="mt-4">
{failMessage({
numberComplete: state.numberComplete,
amount,
type: state.mode,
})}
</h4>
<div>The correct answer was:</div>
<div className="my-2">
<DisplayCorrectAnswer question={state.current} />
{(state.mode === "timeout" || state.mode === "fail") &&
state.current && (
<div className="mb-4">
<h4 className="mt-4">
{failMessage({
numberComplete: state.numberComplete,
amount,
type: state.mode,
})}
</h4>
<div>The correct answer was:</div>
<div className="my-2">
<DisplayCorrectAnswer question={state.current} />
</div>
<div className="my-3">
<ActionButtons />
</div>
</div>
<div className="my-3">
<ActionButtons />
</div>
</div>
)}
)}
</div>
</Reward>
</div>

View File

@ -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],

View File

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

View File

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