Skip to content

Commit 51c5be4

Browse files
tdragontikhdm
andauthored
feat: #37 add programming mode for guided lesson (#174)
Co-authored-by: Dmitrii <[email protected]>
1 parent 956d8a1 commit 51c5be4

File tree

9 files changed

+184
-8
lines changed

9 files changed

+184
-8
lines changed

packages/keybr-intl/lib/messages/en.json

+1-1
Large diffs are not rendered by default.

packages/keybr-intl/translations/en.json

+2
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,8 @@
363363
"settings.previewLesson.label": "Lesson Preview",
364364
"settings.punctuation.description": "Adjust the amount of basic punctuation characters added to the lesson text. Use this option to practice typing the punctuation characters. We recommend to increase this value only if you have all letters above the target speed.",
365365
"settings.punctuation.label": "Add punctuation characters:",
366+
"settings.programming.label": "Add programming characters:",
367+
"settings.programming.description": "Add programming characters to the lesson text. Use this option to practice typing the programming characters. We recommend to increase this value only if you have all letters above the target speed.",
366368
"settings.recoverKeys.description": "When you focus on a new key, it is very likely that the speed of previous keys will decrease. If this option is disabled, you unlock a new key by raising only the focused key above the target speed. If this option is enabled, you will have to raise the focused key and all the previous keys above the target speed. This will make unlocking new keys harder. However, this will also make forgetting old keys harder.",
367369
"settings.recoverKeys.label": "The previous keys are also above the target speed",
368370
"settings.recoverKeys.prefix": "Unlock a next key only when:",

packages/keybr-lesson-ui/lib/Indicators.module.less

+1
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@
2626
.value {
2727
display: flex;
2828
flex: 1;
29+
flex-wrap: wrap;
2930
align-items: center;
3031
}

packages/keybr-lesson/lib/guided.ts

+49-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type WeightedCodePointSet } from "@keybr/keyboard";
33
import { Filter, Letter, type PhoneticModel } from "@keybr/phonetic-model";
44
import { type KeyStatsMap, newKeyStatsMap, type Result } from "@keybr/result";
55
import { type Settings } from "@keybr/settings";
6+
import { type CodePointSet } from "@keybr/unicode";
67
import { Dictionary, filterWordList } from "./dictionary.ts";
78
import { LessonKey, LessonKeys } from "./key.ts";
89
import { Lesson } from "./lesson.ts";
@@ -19,6 +20,8 @@ import {
1920

2021
export class GuidedLesson extends Lesson {
2122
readonly dictionary: Dictionary;
23+
readonly programmingMode: boolean;
24+
readonly programmingCodePoint: CodePointSet;
2225

2326
constructor(
2427
settings: Settings,
@@ -30,17 +33,30 @@ export class GuidedLesson extends Lesson {
3033
this.dictionary = new Dictionary(
3134
filterWordList(wordList, codePoints).filter((word) => word.length > 2),
3235
);
36+
this.programmingMode = settings.get(lessonProps.programming);
37+
this.programmingCodePoint = new Set(
38+
Letter.programming.map(Letter.codePointOf),
39+
);
3340
}
3441

3542
override analyze(results: readonly Result[]): KeyStatsMap {
43+
if (this.programmingMode) {
44+
return newKeyStatsMap(
45+
this.model.letters.concat(Letter.programming),
46+
results,
47+
);
48+
}
3649
return newKeyStatsMap(this.model.letters, results);
3750
}
3851

3952
override update(keyStatsMap: KeyStatsMap): LessonKeys {
4053
const alphabetSize = this.settings.get(lessonProps.guided.alphabetSize);
4154
const recoverKeys = this.settings.get(lessonProps.guided.recoverKeys);
4255

43-
const letters = this.getLetters();
56+
let letters = this.getLetters();
57+
if (this.programmingMode) {
58+
letters = letters.concat(Letter.programming);
59+
}
4460

4561
const minSize = 6;
4662
const maxSize =
@@ -106,16 +122,31 @@ export class GuidedLesson extends Lesson {
106122
}
107123

108124
override generate(lessonKeys: LessonKeys): string {
125+
const includedKeys = lessonKeys.findIncludedKeys();
126+
127+
const mainLessonKeys = includedKeys.filter(
128+
({ letter }) => !this.programmingCodePoint.has(letter.codePoint),
129+
);
130+
131+
const lessonKey = lessonKeys.findFocusedKey();
132+
109133
const wordGenerator = this.makeWordGenerator(
110-
new Filter(lessonKeys.findIncludedKeys(), lessonKeys.findFocusedKey()),
134+
new Filter(
135+
mainLessonKeys,
136+
this.programmingCodePoint.has(lessonKey?.letter.codePoint ?? 0)
137+
? null
138+
: lessonKey,
139+
),
111140
);
112141
const words = mangledWords(
113142
uniqueWords(wordGenerator),
114143
this.model.language,
115-
Letter.restrict(Letter.punctuators, this.codePoints),
144+
this.getSpecialLetters(includedKeys),
116145
{
117146
withCapitals: this.settings.get(lessonProps.capitals),
118-
withPunctuators: this.settings.get(lessonProps.punctuators),
147+
withPunctuators: this.programmingMode
148+
? 1
149+
: this.settings.get(lessonProps.punctuators),
119150
},
120151
this.rng,
121152
);
@@ -136,6 +167,20 @@ export class GuidedLesson extends Lesson {
136167
}
137168
}
138169

170+
private getSpecialLetters(includedKeys: LessonKey[]): Letter[] {
171+
if (this.programmingMode) {
172+
const includedProgrammigCodePoints = new Set(
173+
includedKeys
174+
.filter(({ letter }) =>
175+
this.programmingCodePoint.has(letter.codePoint),
176+
)
177+
.map(({ letter }) => Letter.codePointOf(letter)),
178+
);
179+
return Letter.restrict(Letter.programming, includedProgrammigCodePoints);
180+
}
181+
return Letter.restrict(Letter.punctuators, this.codePoints);
182+
}
183+
139184
private makeWordGenerator(filter: Filter): WordGenerator {
140185
const pseudoWords = phoneticWords(this.model, filter, this.rng);
141186
if (this.settings.get(lessonProps.guided.naturalWords)) {

packages/keybr-lesson/lib/settings.ts

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const lessonProps = {
3535
} as const,
3636
capitals: numberProp("lesson.capitals", 0, { min: 0, max: 1 }),
3737
punctuators: numberProp("lesson.punctuators", 0, { min: 0, max: 1 }),
38+
programming: booleanProp("lesson.programming", false),
3839
doubleWords: booleanProp("lesson.doubleWords", false),
3940
targetSpeed: numberProp("lesson.targetSpeed", 175, { min: 75, max: 750 }),
4041
dailyGoal: numberProp("lesson.dailyGoal", 30, { min: 0, max: 120 }),

packages/keybr-lesson/lib/text/words.test.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
import { Language } from "@keybr/keyboard";
2+
import { Letter } from "@keybr/phonetic-model";
13
import { FakeRNGStream } from "@keybr/rand";
24
import test from "ava";
3-
import { randomWords, uniqueWords, wordSequence } from "./words.ts";
5+
import {
6+
mangledWords,
7+
randomWords,
8+
uniqueWords,
9+
wordSequence,
10+
} from "./words.ts";
411

512
test("random words", (t) => {
613
const rng = FakeRNGStream(3);
@@ -44,3 +51,17 @@ test("unique words", (t) => {
4451
t.is(words(), "a");
4552
t.is(words(), "b");
4653
});
54+
55+
test("mangle words for programming without special symbols", (t) => {
56+
const rng = FakeRNGStream(3);
57+
const wordList = mangledWords(
58+
wordSequence(["a", "b", "c"], { wordIndex: 0 }),
59+
Language.EN,
60+
[],
61+
{ withCapitals: 0, withPunctuators: 1 },
62+
rng,
63+
);
64+
t.is(wordList(), "a");
65+
t.is(wordList(), "b");
66+
t.is(wordList(), "c");
67+
});

packages/keybr-lesson/lib/text/words.ts

+49-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ export function mangledWords(
9292
if (withCapitals > 0 && withCapitals >= random()) {
9393
word = language.capitalCase(word);
9494
}
95-
if (withPunctuators > 0 && withPunctuators >= random()) {
95+
if (
96+
punctuators.length !== 0 &&
97+
withPunctuators > 0 &&
98+
withPunctuators >= random()
99+
) {
96100
const { codePoint } = weightedRandomSample(
97101
punctuators,
98102
({ f }) => f,
@@ -126,6 +130,50 @@ export function mangledWords(
126130
case 63:
127131
word = `${word}?`;
128132
break;
133+
// Programming specific symbols.
134+
case 40:
135+
case 41:
136+
word = `(${word})`;
137+
break;
138+
case 123:
139+
case 125:
140+
word = `{${word}}`;
141+
break;
142+
case 61:
143+
word = `${word}=${nextWord()}`;
144+
break;
145+
case 43:
146+
word = `${word}+${nextWord()}`;
147+
break;
148+
case 42:
149+
word = `*${word}`;
150+
break;
151+
case 47:
152+
word = `${word}/${nextWord()}`;
153+
break;
154+
case 37:
155+
word = `${word}%`;
156+
break;
157+
case 124:
158+
word = `${word}|${nextWord()}`;
159+
break;
160+
case 38:
161+
word = `${word}&${nextWord()}`;
162+
break;
163+
case 60:
164+
case 62:
165+
word = `<${word}>`;
166+
break;
167+
case 91:
168+
case 93:
169+
word = `[${word}]`;
170+
break;
171+
case 95:
172+
word = `${word}_${nextWord()}`;
173+
break;
174+
default:
175+
word = `${word}${String.fromCodePoint(codePoint)}`;
176+
break;
129177
}
130178
}
131179
return word;

packages/keybr-phonetic-model/lib/letter.ts

+36
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,42 @@ export namespace Letter {
8080
new Letter(/* ~ */ 0x007e, 1),
8181
];
8282

83+
// For paired characters, the frequency is halved to account for the fact that they are used in pairs.
84+
export const programming: readonly Letter[] = [
85+
new Letter(/* ( */ 0x0028, 2.5),
86+
new Letter(/* ) */ 0x0029, 2.4),
87+
new Letter(/* { */ 0x007b, 1.8),
88+
new Letter(/* } */ 0x007d, 1.7),
89+
new Letter(/* " */ 0x0022, 1.7),
90+
new Letter(/* ' */ 0x0027, 1.7),
91+
new Letter(/* , */ 0x002c, 1.6),
92+
new Letter(/* ; */ 0x003b, 1.5),
93+
new Letter(/* [ */ 0x005b, 1.5),
94+
new Letter(/* ] */ 0x005d, 1.5),
95+
new Letter(/* : */ 0x003a, 1.4),
96+
new Letter(/* . */ 0x002e, 1.3),
97+
new Letter(/* = */ 0x003d, 1.2),
98+
new Letter(/* + */ 0x002b, 1.1),
99+
new Letter(/* - */ 0x002d, 1.0),
100+
new Letter(/* * */ 0x002a, 0.9),
101+
new Letter(/* / */ 0x002f, 0.8),
102+
new Letter(/* % */ 0x0025, 0.7),
103+
new Letter(/* | */ 0x007c, 0.6),
104+
new Letter(/* & */ 0x0026, 0.5),
105+
new Letter(/* ! */ 0x0021, 0.4),
106+
new Letter(/* ? */ 0x003f, 0.3),
107+
new Letter(/* < */ 0x003c, 0.2),
108+
new Letter(/* > */ 0x003e, 0.1),
109+
new Letter(/* _ */ 0x005f, 0.1),
110+
new Letter(/* # */ 0x0023, 0.1),
111+
new Letter(/* @ */ 0x0040, 0.1),
112+
new Letter(/* $ */ 0x0024, 0.1),
113+
new Letter(/* ^ */ 0x005e, 0.1),
114+
new Letter(/* ~ */ 0x007e, 0.1),
115+
new Letter(/* \ */ 0x005c, 0.1),
116+
new Letter(/* ` */ 0x0060, 0.1),
117+
];
118+
83119
export const codePointOf = ({ codePoint }: Letter): CodePoint => {
84120
return codePoint;
85121
};

packages/page-practice/lib/settings/lesson/TextManglingProp.tsx

+23-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useIntlNumbers } from "@keybr/intl";
22
import { lessonProps } from "@keybr/lesson";
33
import { useSettings } from "@keybr/settings";
44
import {
5+
CheckBox,
56
Explainer,
67
Field,
78
FieldList,
@@ -10,11 +11,12 @@ import {
1011
Value,
1112
} from "@keybr/widget";
1213
import { type ReactNode } from "react";
13-
import { FormattedMessage } from "react-intl";
14+
import { FormattedMessage, useIntl } from "react-intl";
1415

1516
export function TextManglingProp(): ReactNode {
1617
const { formatPercents } = useIntlNumbers();
1718
const { settings, updateSettings } = useSettings();
19+
const { formatMessage } = useIntl();
1820
return (
1921
<>
2022
<FieldList>
@@ -79,6 +81,26 @@ export function TextManglingProp(): ReactNode {
7981
defaultMessage="Adjust the amount of basic punctuation characters added to the lesson text. Use this option to practice typing the punctuation characters. We recommend to increase this value only if you have all letters above the target speed."
8082
/>
8183
</Explainer>
84+
<FieldList>
85+
<Field>
86+
<CheckBox
87+
label={formatMessage({
88+
id: "settings.programming.label",
89+
defaultMessage: "Add programming characters",
90+
})}
91+
checked={settings.get(lessonProps.programming)}
92+
onChange={(value) => {
93+
updateSettings(settings.set(lessonProps.programming, value));
94+
}}
95+
/>
96+
</Field>
97+
</FieldList>
98+
<Explainer>
99+
<FormattedMessage
100+
id="settings.programming.description"
101+
defaultMessage="Add programming characters to the lesson text. Use this option to practice typing the programming characters. Programming characters will be added only if you have all letters above the target speed."
102+
/>
103+
</Explainer>
82104
</>
83105
);
84106
}

0 commit comments

Comments
 (0)