From 0e24e8c64915715d3092f03a190773d5cca9d1ae Mon Sep 17 00:00:00 2001 From: pepaskopec-blip Date: Tue, 6 Oct 2026 14:49:25 +0200 Subject: [PATCH] Add interactive mathematics for elementary review and four secondary-school years. Co-authored-by: Cursor --- README.md | 8 +- .../org/maturita/maturita/AppViewModel.kt | 25 + .../org/maturita/maturita/MainActivity.kt | 73 + .../main/java/org/maturita/maturita/Route.kt | 73 +- .../org/maturita/maturita/data/Answers.kt | 653 ++++++ .../org/maturita/maturita/data/Content.kt | 10 + .../maturita/maturita/data/ProgressStore.kt | 50 + .../org/maturita/maturita/ui/Exercises.kt | 205 ++ .../java/org/maturita/maturita/ui/Screens.kt | 51 + data/changelog.txt | 6 + ios/Graduately/AppModel.swift | 25 + ios/Graduately/Data/Answers.swift | 655 ++++++ ios/Graduately/Data/Content.swift | 15 + ios/Graduately/Data/ProgressStore.swift | 50 + ios/Graduately/Route.swift | 100 + ios/Graduately/UI/Components.swift | 1 + ios/Graduately/UI/ExercisesMore.swift | 249 ++ ios/Graduately/UI/Screens.swift | 160 ++ scripts/extract-android-content.py | 55 +- scripts/gen-math.py | 2040 +++++++++++++++++ src/graduately.h | 45 + src/i18n.c | 11 +- src/icons.c | 28 + src/main.c | 14 + src/mat_year0.c | 469 ++++ src/mat_year1.c | 468 ++++ src/mat_year2.c | 468 ++++ src/mat_year3.c | 468 ++++ src/mat_year4.c | 472 ++++ src/matematika.c | 955 ++++++++ src/math_check.c | 1362 +++++++++++ src/math_i18n.inc | 176 ++ src/progress.c | 1 + src/search.c | 45 +- src/stats.c | 6 + src/subjects.c | 10 +- src/theme.c | 1 + src/util.c | 15 + 38 files changed, 9507 insertions(+), 11 deletions(-) create mode 100644 scripts/gen-math.py create mode 100644 src/mat_year0.c create mode 100644 src/mat_year1.c create mode 100644 src/mat_year2.c create mode 100644 src/mat_year3.c create mode 100644 src/mat_year4.c create mode 100644 src/matematika.c create mode 100644 src/math_check.c create mode 100644 src/math_i18n.inc diff --git a/README.md b/README.md index da24963..c7a9d21 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses - **Český jazyk a literatura** — literatura (4 ročníky, 32 lekcí podle osnov středních škol), mluvnice (20 cvičení) a maturitní četba (82 knih: zápisky, kvíz, sestavení děje) +- **Matematika** — opakování základní školy a 4 ročníky, 40 lekcí; + příklady se píšou a kontrolují, u geometrie jde i rýsovat na mřížku - **Fyzika** — 4 ročníky, 32 lekcí podle osnov středních škol (mechanika, teplo a vlny, elektřina a magnetismus, optika až astrofyzika) - **Základy přírodopisných věd** — chemie a biologie, v každé 8 lekcí @@ -82,7 +84,7 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses ### Použití 1. **Pokračuj** otevře mapu předmětů. -2. Otevřený předmět (vlajka, Wi‑Fi, čip, česká vlajka, váhy, atom, baňka) vede na učební cestu. +2. Otevřený předmět (vlajka, Wi‑Fi, čip, česká vlajka, váhy, funkce, atom, baňka) vede na učební cestu. 3. Uzly cvičení / lekcí otevřou obsah; zamčené nic nedělají. 4. **Hledat** (lupa), **Statistiky** (graf) a **Nastavení** (ozubené kolo) jsou vpravo nahoře. @@ -270,6 +272,8 @@ Intel Mac, another architecture, or hacking on the code → - **Czech language** — literature (4 years, 32 lessons from the secondary-school syllabus), grammar (20 exercises) and required reading (82 books: notes, quiz, plot ordering) +- **Mathematics** — elementary-school review and 4 years, 40 lessons; + examples are typed and checked, and some geometry is drawn on a grid - **Physics** — 4 years, 32 lessons from the secondary-school syllabus (mechanics, heat and waves, electricity and magnetism, optics through astrophysics) - **Natural sciences** — chemistry and biology, eight lessons each @@ -284,7 +288,7 @@ Intel Mac, another architecture, or hacking on the code → ### Usage 1. **Continue** opens the subject map. -2. An open subject (flag, Wi‑Fi, chip, Czech flag, scales, atom, flask) opens its path. +2. An open subject (flag, Wi‑Fi, chip, Czech flag, scales, function, atom, flask) opens its path. 3. Exercise / lesson nodes open content; locked nodes do nothing. 4. **Search** (magnifier), **Statistics** (chart) and **Settings** (gear) sit in the top-right. diff --git a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt index c99db28..4792ee8 100644 --- a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt +++ b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt @@ -191,6 +191,31 @@ class AppViewModel(app: Application) : AndroidViewModel(app) { refresh() } + fun markMat0(id: Int) { + progress.markMat0(id) + refresh() + } + + fun markMat(id: Int) { + progress.markMat(id) + refresh() + } + + fun markMat2(id: Int) { + progress.markMat2(id) + refresh() + } + + fun markMat3(id: Int) { + progress.markMat3(id) + refresh() + } + + fun markMat4(id: Int) { + progress.markMat4(id) + refresh() + } + fun markFyz4(id: Int) { progress.markFyz4(id) refresh() diff --git a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt index 2ac0cab..5d61e24 100644 --- a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt +++ b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt @@ -33,6 +33,8 @@ import org.maturita.maturita.ui.HwQuizScreen import org.maturita.maturita.ui.HwYearsScreen import org.maturita.maturita.ui.LessonListScreen import org.maturita.maturita.ui.FyzQuizScreen +import org.maturita.maturita.ui.MathPracticeScreen +import org.maturita.maturita.ui.MatYearsScreen import org.maturita.maturita.ui.FyzYearsScreen import org.maturita.maturita.ui.LitYearsScreen import org.maturita.maturita.ui.LessonRow @@ -350,6 +352,77 @@ private fun MaturitaAppBody(vm: AppViewModel) { } } is Route.Lit4Ex -> LitQuizScreen(vm, 4, r.id) + Route.MatYears -> MatYearsScreen(vm) + Route.Mat0Map -> LessonListScreen( + vm, vm.tr("mat0_year"), vm.tr("mat0_sub"), + vm.content.mat0.map { l -> + val id = l.int("id") + LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat0Done(id), Route.Mat0Lesson(id)) + }, + ) + is Route.Mat0Lesson -> { + val l = vm.content.mat0Lesson(r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.Mat0Ex(r.id)) + } + } + is Route.Mat0Ex -> MathPracticeScreen(vm, 0, r.id) + Route.MatMap -> LessonListScreen( + vm, vm.tr("mat_year1"), vm.tr("mat_sub"), + vm.content.mat.map { l -> + val id = l.int("id") + LessonRow(vm.tr(l.str("titleKey")), vm.progress.matDone(id), Route.MatLesson(id)) + }, + ) + is Route.MatLesson -> { + val l = vm.content.matLesson(r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.MatEx(r.id)) + } + } + is Route.MatEx -> MathPracticeScreen(vm, 1, r.id) + Route.Mat2Map -> LessonListScreen( + vm, vm.tr("mat_year2"), vm.tr("mat2_sub"), + vm.content.mat2.map { l -> + val id = l.int("id") + LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat2Done(id), Route.Mat2Lesson(id)) + }, + ) + is Route.Mat2Lesson -> { + val l = vm.content.mat2Lesson(r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.Mat2Ex(r.id)) + } + } + is Route.Mat2Ex -> MathPracticeScreen(vm, 2, r.id) + Route.Mat3Map -> LessonListScreen( + vm, vm.tr("mat_year3"), vm.tr("mat3_sub"), + vm.content.mat3.map { l -> + val id = l.int("id") + LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat3Done(id), Route.Mat3Lesson(id)) + }, + ) + is Route.Mat3Lesson -> { + val l = vm.content.mat3Lesson(r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.Mat3Ex(r.id)) + } + } + is Route.Mat3Ex -> MathPracticeScreen(vm, 3, r.id) + Route.Mat4Map -> LessonListScreen( + vm, vm.tr("mat_year4"), vm.tr("mat4_sub"), + vm.content.mat4.map { l -> + val id = l.int("id") + LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat4Done(id), Route.Mat4Lesson(id)) + }, + ) + is Route.Mat4Lesson -> { + val l = vm.content.mat4Lesson(r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.Mat4Ex(r.id)) + } + } + is Route.Mat4Ex -> MathPracticeScreen(vm, 4, r.id) Route.FyzYears -> FyzYearsScreen(vm) Route.FyzMap -> LessonListScreen( vm, vm.tr("fyz_year1"), vm.tr("fyz_sub"), diff --git a/android/app/src/main/java/org/maturita/maturita/Route.kt b/android/app/src/main/java/org/maturita/maturita/Route.kt index 9862a1e..8f1a030 100644 --- a/android/app/src/main/java/org/maturita/maturita/Route.kt +++ b/android/app/src/main/java/org/maturita/maturita/Route.kt @@ -50,6 +50,22 @@ sealed class Route(val page: String) { data object BioMap : Route("biomap") data class BioLesson(val id: Int) : Route("biounit$id") data class BioEx(val id: Int) : Route("bioex$id") + data object MatYears : Route("matyears") + data object Mat0Map : Route("mat0map") + data class Mat0Lesson(val id: Int) : Route("mat0unit$id") + data class Mat0Ex(val id: Int) : Route("mat0ex$id") + data object MatMap : Route("matmap") + data class MatLesson(val id: Int) : Route("matunit$id") + data class MatEx(val id: Int) : Route("matex$id") + data object Mat2Map : Route("mat2map") + data class Mat2Lesson(val id: Int) : Route("mat2unit$id") + data class Mat2Ex(val id: Int) : Route("mat2ex$id") + data object Mat3Map : Route("mat3map") + data class Mat3Lesson(val id: Int) : Route("mat3unit$id") + data class Mat3Ex(val id: Int) : Route("mat3ex$id") + data object Mat4Map : Route("mat4map") + data class Mat4Lesson(val id: Int) : Route("mat4unit$id") + data class Mat4Ex(val id: Int) : Route("mat4ex$id") data object FyzYears : Route("fyzyears") data object FyzMap : Route("fyzmap") data class FyzLesson(val id: Int) : Route("fyzunit$id") @@ -83,7 +99,10 @@ sealed class Route(val page: String) { this is Lit3Lesson || this is Lit3Ex || this is Lit4Lesson || this is Lit4Ex || this is ChemLesson || this is ChemEx || this is BioLesson || this is BioEx || this is FyzLesson || this is FyzEx || this is Fyz2Lesson || this is Fyz2Ex || - this is Fyz3Lesson || this is Fyz3Ex || this is Fyz4Lesson || this is Fyz4Ex + this is Fyz3Lesson || this is Fyz3Ex || this is Fyz4Lesson || this is Fyz4Ex || + this is Mat0Lesson || this is Mat0Ex || this is MatLesson || this is MatEx || + this is Mat2Lesson || this is Mat2Ex || this is Mat3Lesson || this is Mat3Ex || + this is Mat4Lesson || this is Mat4Ex /** Full stack from the practice root to this screen, so back always goes up one level. */ fun stack(): List = when (this) { @@ -135,6 +154,22 @@ sealed class Route(val page: String) { BioMap -> listOf(Subjects, SciMap, BioMap) is BioLesson -> listOf(Subjects, SciMap, BioMap, this) is BioEx -> listOf(Subjects, SciMap, BioMap, BioLesson(id), this) + MatYears -> listOf(Subjects, MatYears) + Mat0Map -> listOf(Subjects, MatYears, Mat0Map) + is Mat0Lesson -> listOf(Subjects, MatYears, Mat0Map, this) + is Mat0Ex -> listOf(Subjects, MatYears, Mat0Map, Mat0Lesson(id), this) + MatMap -> listOf(Subjects, MatYears, MatMap) + is MatLesson -> listOf(Subjects, MatYears, MatMap, this) + is MatEx -> listOf(Subjects, MatYears, MatMap, MatLesson(id), this) + Mat2Map -> listOf(Subjects, MatYears, Mat2Map) + is Mat2Lesson -> listOf(Subjects, MatYears, Mat2Map, this) + is Mat2Ex -> listOf(Subjects, MatYears, Mat2Map, Mat2Lesson(id), this) + Mat3Map -> listOf(Subjects, MatYears, Mat3Map) + is Mat3Lesson -> listOf(Subjects, MatYears, Mat3Map, this) + is Mat3Ex -> listOf(Subjects, MatYears, Mat3Map, Mat3Lesson(id), this) + Mat4Map -> listOf(Subjects, MatYears, Mat4Map) + is Mat4Lesson -> listOf(Subjects, MatYears, Mat4Map, this) + is Mat4Ex -> listOf(Subjects, MatYears, Mat4Map, Mat4Lesson(id), this) FyzYears -> listOf(Subjects, FyzYears) FyzMap -> listOf(Subjects, FyzYears, FyzMap) is FyzLesson -> listOf(Subjects, FyzYears, FyzMap, this) @@ -181,6 +216,12 @@ fun routeFromPage(page: String): Route? = when (page) { "scimap" -> Route.SciMap "chemmap" -> Route.ChemMap "biomap" -> Route.BioMap + "matyears" -> Route.MatYears + "mat0map" -> Route.Mat0Map + "matmap" -> Route.MatMap + "mat2map" -> Route.Mat2Map + "mat3map" -> Route.Mat3Map + "mat4map" -> Route.Mat4Map "fyzyears" -> Route.FyzYears "fyzmap" -> Route.FyzMap "fyz2map" -> Route.Fyz2Map @@ -229,6 +270,36 @@ fun routeFromPage(page: String): Route? = when (page) { Regex("""bioex(\d+)""").matchEntire(page)?.let { return Route.BioEx(it.groupValues[1].toInt()) } + Regex("""mat0unit(\d+)""").matchEntire(page)?.let { + return Route.Mat0Lesson(it.groupValues[1].toInt()) + } + Regex("""mat0ex(\d+)""").matchEntire(page)?.let { + return Route.Mat0Ex(it.groupValues[1].toInt()) + } + Regex("""mat4unit(\d+)""").matchEntire(page)?.let { + return Route.Mat4Lesson(it.groupValues[1].toInt()) + } + Regex("""mat4ex(\d+)""").matchEntire(page)?.let { + return Route.Mat4Ex(it.groupValues[1].toInt()) + } + Regex("""mat3unit(\d+)""").matchEntire(page)?.let { + return Route.Mat3Lesson(it.groupValues[1].toInt()) + } + Regex("""mat3ex(\d+)""").matchEntire(page)?.let { + return Route.Mat3Ex(it.groupValues[1].toInt()) + } + Regex("""mat2unit(\d+)""").matchEntire(page)?.let { + return Route.Mat2Lesson(it.groupValues[1].toInt()) + } + Regex("""mat2ex(\d+)""").matchEntire(page)?.let { + return Route.Mat2Ex(it.groupValues[1].toInt()) + } + Regex("""matunit(\d+)""").matchEntire(page)?.let { + return Route.MatLesson(it.groupValues[1].toInt()) + } + Regex("""matex(\d+)""").matchEntire(page)?.let { + return Route.MatEx(it.groupValues[1].toInt()) + } Regex("""fyz4unit(\d+)""").matchEntire(page)?.let { return Route.Fyz4Lesson(it.groupValues[1].toInt()) } diff --git a/android/app/src/main/java/org/maturita/maturita/data/Answers.kt b/android/app/src/main/java/org/maturita/maturita/data/Answers.kt index 6104b1f..6c81fbd 100644 --- a/android/app/src/main/java/org/maturita/maturita/data/Answers.kt +++ b/android/app/src/main/java/org/maturita/maturita/data/Answers.kt @@ -63,3 +63,656 @@ fun hasUmlaut(s: String?): Boolean { return s.any { it == 'ä' || it == 'ö' || it == 'ü' || it == 'ß' || it == 'Ä' || it == 'Ö' || it == 'Ü' } } + +// Same rules as src/math_check.c. Do not route numbers through normalizeAnswer. + +private data class MFrac(val n: Long = 0, val d: Long = 1) + +private class MTerm( + var ok: Boolean = true, + var a: MFrac = MFrac(), + var b: MFrac = MFrac(), + var rad: Long = 0, + var p: MFrac = MFrac(), +) + +private fun mGcd(a0: Long, b0: Long): Long { + var a = if (a0 < 0) -a0 else a0 + var b = if (b0 < 0) -b0 else b0 + while (b != 0L) { + val t = a % b + a = b + b = t + } + return if (a == 0L) 1 else a +} + +private fun mMul(a: Long, b: Long): Long? = try { + Math.multiplyExact(a, b) +} catch (_: ArithmeticException) { + null +} + +private fun mAddL(a: Long, b: Long): Long? = try { + Math.addExact(a, b) +} catch (_: ArithmeticException) { + null +} + +private fun mSet(n0: Long, d0: Long): MFrac? { + if (d0 == 0L) return null + var n = n0 + var d = d0 + if (d < 0) { + n = -n + d = -d + } + val g = mGcd(n, d) + return MFrac(n / g, d / g) +} + +private fun mAddF(x: MFrac, y: MFrac): MFrac? { + val xn = mMul(x.n, y.d) ?: return null + val yn = mMul(y.n, x.d) ?: return null + val d = mMul(x.d, y.d) ?: return null + val s = mAddL(xn, yn) ?: return null + return mSet(s, d) +} + +private fun mMulF(x: MFrac, y: MFrac): MFrac? { + val n = mMul(x.n, y.n) ?: return null + val d = mMul(x.d, y.d) ?: return null + return mSet(n, d) +} + +private fun mDivF(x: MFrac, y: MFrac): MFrac? { + if (y.n == 0L) return null + return mMulF(x, MFrac(y.d, y.n)) +} + +private fun mSameF(x: MFrac, y: MFrac) = x.n == y.n && x.d == y.d + +private fun mPow(b: Long, e: Int): Long { + var r = 1L + var i = 0 + while (i < e) { + r *= b + i++ + } + return r +} + +private fun mSqrtReduce(n0: Long): Pair? { + if (n0 < 0 || n0 > 1_000_000) return null + var n = n0 + var out = 1L + var f = 2L + while (f * f <= n) { + var c = 0 + while (n % f == 0L) { + n /= f + c++ + } + if (c >= 2) out *= mPow(f, c / 2) + if (c % 2 == 1) n *= f + f = if (f == 2L) 3 else f + 2 + if (f > 10000) break + } + return out to n +} + +private fun mNorm(t: MTerm) { + if (!t.ok) return + if (t.rad > 1) { + val s = mSqrtReduce(t.rad) ?: run { t.ok = false; return } + t.b = mMulF(t.b, MFrac(s.first, 1)) ?: run { t.ok = false; return } + t.rad = s.second + } + if (t.rad <= 1) { + t.a = mAddF(t.a, t.b) ?: run { t.ok = false; return } + t.b = MFrac() + t.rad = 0 + } + if (t.b.n == 0L) t.rad = 0 +} + +private fun mFail() = MTerm(ok = false) + +private fun mAdd(x: MTerm, y: MTerm): MTerm { + if (!x.ok || !y.ok) return mFail() + val o = MTerm() + o.a = mAddF(x.a, y.a) ?: return mFail() + o.p = mAddF(x.p, y.p) ?: return mFail() + if (x.rad == 0L && y.rad == 0L) { + o.b = MFrac() + o.rad = 0 + } else if (x.rad == 0L) { + o.b = y.b + o.rad = y.rad + } else if (y.rad == 0L || x.rad == y.rad) { + o.b = mAddF(x.b, y.b) ?: return mFail() + o.rad = x.rad + } else return mFail() + mNorm(o) + return o +} + +private fun mScale(x: MTerm, f: MFrac): MTerm { + if (!x.ok) return mFail() + val o = MTerm() + o.a = mMulF(x.a, f) ?: return mFail() + o.b = mMulF(x.b, f) ?: return mFail() + o.p = mMulF(x.p, f) ?: return mFail() + o.rad = x.rad + mNorm(o) + return o +} + +private fun mMulT(x: MTerm, y: MTerm): MTerm { + if (!x.ok || !y.ok) return mFail() + if ((x.p.n != 0L && (y.p.n != 0L || y.rad != 0L)) || + (y.p.n != 0L && (x.p.n != 0L || x.rad != 0L)) + ) return mFail() + if (x.rad != 0L && y.rad != 0L && x.rad != y.rad) return mFail() + val o = MTerm() + o.a = mMulF(x.a, y.a) ?: return mFail() + if (x.rad == 0L && y.rad == 0L) { + o.b = MFrac() + o.rad = 0 + } else if (x.rad == 0L) { + o.b = mMulF(x.a, y.b) ?: return mFail() + o.rad = y.rad + } else if (y.rad == 0L) { + o.b = mMulF(y.a, x.b) ?: return mFail() + o.rad = x.rad + } else { + val ab = mMulF(x.a, y.b) ?: return mFail() + val ba = mMulF(x.b, y.a) ?: return mFail() + val cross = mAddF(ab, ba) ?: return mFail() + val sq = mMul(x.rad, y.rad) ?: return mFail() + val bb = mMulF(x.b, y.b) ?: return mFail() + val extra = mMulF(bb, MFrac(sq, 1)) ?: return mFail() + o.a = mAddF(o.a, extra) ?: return mFail() + o.b = cross + o.rad = x.rad + } + o.p = if (x.p.n != 0L) x.p else y.p + if ((y.a.n != 1L || y.a.d != 1L || y.b.n != 0L || y.rad != 0L || y.p.n != 0L) && x.p.n != 0L) { + o.p = mMulF(x.p, y.a) ?: return mFail() + } + if ((x.a.n != 1L || x.a.d != 1L || x.b.n != 0L || x.rad != 0L || x.p.n != 0L) && + y.p.n != 0L && x.p.n == 0L + ) { + o.p = mMulF(y.p, x.a) ?: return mFail() + } + mNorm(o) + return o +} + +private fun mDivT(x: MTerm, y: MTerm): MTerm { + if (!x.ok || !y.ok || y.p.n != 0L) return mFail() + if (y.rad == 0L) { + val f = mDivF(MFrac(1, 1), y.a) ?: return mFail() + return mScale(x, f) + } + if (x.p.n != 0L) return mFail() + val yb = mMulF(y.b, MFrac(y.rad, 1)) ?: return mFail() + val bb = mMulF(y.b, yb) ?: return mFail() + val aa = mMulF(y.a, y.a) ?: return mFail() + val den = mAddF(aa, MFrac(-bb.n, bb.d)) ?: return mFail() + val num = MTerm() + num.a = y.a + num.b = MFrac(-y.b.n, y.b.d) + num.rad = y.rad + val inv = mDivF(MFrac(1, 1), den) ?: return mFail() + return mMulT(x, mScale(num, inv)) +} + +private sealed class MTok { + class Num(val n: Long, val d: Long) : MTok() + object Sqrt : MTok() + object Pi : MTok() + object Add : MTok() + object Sub : MTok() + object Mul : MTok() + object Div : MTok() + object Lp : MTok() + object Rp : MTok() +} + +private fun mLex(s: String): List? { + val toks = ArrayList() + var i = 0 + fun peek(k: Int) = if (i + k < s.length) s[i + k] else null + while (i < s.length) { + while (i < s.length && s[i].isWhitespace()) i++ + if (i >= s.length) break + val ch = s[i] + if (ch.isDigit() || ((ch == '.' || ch == ',') && peek(1)?.isDigit() == true)) { + var whole = 0L + var saw = false + while (i < s.length && s[i].isDigit()) { + saw = true + whole = whole * 10 + (s[i] - '0') + i++ + } + if (i < s.length && (s[i] == '.' || s[i] == ',') && i + 1 < s.length && s[i + 1].isDigit()) { + i++ + var frac = 0L + var div = 1L + while (i < s.length && s[i].isDigit()) { + frac = frac * 10 + (s[i] - '0') + div *= 10 + i++ + } + val n = mAddL(mMul(whole, div) ?: return null, frac) ?: return null + toks.add(MTok.Num(n, div)) + } else if (saw) { + toks.add(MTok.Num(whole, 1)) + } else return null + continue + } + when (ch) { + '+' -> toks.add(MTok.Add) + '-', '\u2212', '\u2013' -> toks.add(MTok.Sub) + '*', '\u00D7', '\u00B7' -> toks.add(MTok.Mul) + '/', ':' -> toks.add(MTok.Div) + '(' -> toks.add(MTok.Lp) + ')' -> toks.add(MTok.Rp) + '\u03C0' -> toks.add(MTok.Pi) + '\u221A' -> toks.add(MTok.Sqrt) + else -> { + if (ch in 'A'..'Z' || ch in 'a'..'z') { + val start = i + while (i < s.length && (s[i] in 'A'..'Z' || s[i] in 'a'..'z')) i++ + when (s.substring(start, i).lowercase()) { + "pi" -> toks.add(MTok.Pi) + "sqrt" -> toks.add(MTok.Sqrt) + else -> return null + } + continue + } + return null + } + } + i++ + } + return toks +} + +private class MParse(val t: List) { + var i = 0 + fun peek() = if (i < t.size) t[i] else null + fun take(): MTok? = if (i < t.size) t[i++] else null + fun at(k: Int) = if (k in t.indices) t[k] else null + + fun expr(): MTerm? { + var left = term() ?: return null + while (true) { + val p = peek() + if (p !is MTok.Add && p !is MTok.Sub) break + take() + val r = term() ?: return null + left = if (p is MTok.Sub) mAdd(left, mScale(r, MFrac(-1, 1))) else mAdd(left, r) + if (!left.ok) return null + } + return left + } + + fun term(): MTerm? { + var left = unary() ?: return null + while (true) { + val p = peek() + if (p !is MTok.Mul && p !is MTok.Div) break + take() + val r = unary() ?: return null + left = if (p is MTok.Div) mDivT(left, r) else mMulT(left, r) + if (!left.ok) return null + } + return left + } + + fun unary(): MTerm? { + if (peek() is MTok.Sub) { + take() + val v = unary() ?: return null + return mScale(v, MFrac(-1, 1)) + } + if (peek() is MTok.Add) { + take() + return unary() + } + return factor() + } + + fun factor(): MTerm? { + var left = primary() ?: return null + while (true) { + when (peek()) { + is MTok.Sqrt, is MTok.Pi, is MTok.Lp -> { + val r = primary() ?: return null + left = mMulT(left, r) + if (!left.ok) return null + } + is MTok.Num -> return null + else -> return left + } + } + } + + fun primary(): MTerm? { + when (val p = take()) { + is MTok.Num -> { + val n = p.n + val d = p.d + if (peek() is MTok.Div && at(i + 1) is MTok.Num) { + take() + val n2 = take() as? MTok.Num ?: return null + if (n2.n <= 0 || n2.d != 1L || d != 1L) return null + val o = MTerm() + o.a = mSet(n, n2.n) ?: return null + return o + } + if (d == 1L && n >= 0 && peek() is MTok.Num) { + val saved = i + val n2 = take() as MTok.Num + val n3tok = at(i + 1) + if (peek() is MTok.Div && n3tok is MTok.Num && n2.d == 1L && n3tok.d == 1L && n2.n >= 0 && n3tok.n > 0) { + take() + take() + val frac = mSet(n2.n, n3tok.n) ?: return null + val whole = MTerm() + whole.a = MFrac(n, 1) + val part = MTerm() + part.a = frac + return mAdd(whole, part) + } + i = saved + } + val o = MTerm() + o.a = mSet(n, d) ?: return null + return o + } + is MTok.Pi -> { + val o = MTerm() + o.p = MFrac(1, 1) + return o + } + is MTok.Sqrt -> { + if (peek() is MTok.Lp) { + take() + val inner = expr() ?: return null + if (take() !is MTok.Rp) return null + return mSqrtTerm(inner) + } + val num = peek() + if (num is MTok.Num && num.d == 1L && num.n >= 0) { + take() + val inner = MTerm() + inner.a = MFrac(num.n, 1) + return mSqrtTerm(inner) + } + return null + } + is MTok.Lp -> { + val inner = expr() ?: return null + if (take() !is MTok.Rp) return null + return inner + } + else -> return null + } + } +} + +private fun mSqrtTerm(inner: MTerm): MTerm? { + if (!inner.ok || inner.p.n != 0L || inner.rad != 0L || inner.b.n != 0L) return null + if (inner.a.n < 0 || inner.a.d != 1L) return null + val s = mSqrtReduce(inner.a.n) ?: return null + val o = MTerm() + o.b = MFrac(s.first, 1) + o.rad = s.second + mNorm(o) + return if (o.ok) o else null +} + +private fun mStripUnits(raw: String): String { + var s = raw.trim() + val units = listOf("procenta", "procent", "cm", "mm", "dm", "km", "m") + var cut = true + while (cut) { + cut = false + s = s.trimEnd() + if (s.endsWith("°") || s.endsWith("%") || s.endsWith(".")) { + s = s.dropLast(1) + cut = true + continue + } + for (u in units) { + if (s.length <= u.length) continue + if (s.lowercase().endsWith(u)) { + val keep = s.dropLast(u.length) + if (keep.lastOrNull()?.isLetter() == true) continue + s = keep + cut = true + break + } + } + } + return s.trim() +} + +private fun mPrep(raw: String): String { + var s = raw.trim() + val pairs = listOf('(' to ')', '[' to ']', '{' to '}') + while (s.length >= 2 && pairs.any { it.first == s.first() && it.second == s.last() }) { + s = s.substring(1, s.length - 1).trim() + } + return mStripUnits(s).trim() +} + +private fun mParseTerm(s: String): MTerm? { + val text = mPrep(s) + if (text.isEmpty()) return null + val toks = mLex(text) ?: return null + if (toks.isEmpty()) return null + val p = MParse(toks) + val v = p.expr() ?: return null + if (p.i != toks.size || !v.ok) return null + return v +} + +private fun mExact(x: MTerm, y: MTerm) = + x.ok && y.ok && mSameF(x.a, y.a) && mSameF(x.b, y.b) && x.rad == y.rad && mSameF(x.p, y.p) + +private fun mDouble(t: MTerm): Double { + var v = t.a.n.toDouble() / t.a.d + t.p.n.toDouble() / t.p.d * Math.PI + if (t.b.n != 0L && t.rad > 0) v += t.b.n.toDouble() / t.b.d * Math.sqrt(t.rad.toDouble()) + return v +} + +private fun mClose(x: MTerm, y: MTerm): Boolean { + if (!x.ok || !y.ok) return false + if (mExact(x, y)) return true + if (x.p.n != 0L || y.p.n != 0L) return false + val diff = Math.abs(mDouble(x) - mDouble(y)) + return if (x.b.n != 0L || y.b.n != 0L) diff <= 0.001 else diff <= 0.0005 +} + +private fun mFold(s: String): String { + val out = StringBuilder() + for (ch in s.lowercase()) { + when (ch) { + 'á', 'à', 'ä', 'â' -> out.append('a') + 'č', 'ć' -> out.append('c') + 'ď' -> out.append('d') + 'é', 'ě', 'ë', 'ê' -> out.append('e') + 'í', 'ï' -> out.append('i') + 'ň' -> out.append('n') + 'ó', 'ö', 'ô' -> out.append('o') + 'ř' -> out.append('r') + 'š', 'ś' -> out.append('s') + 'ť' -> out.append('t') + 'ú', 'ů', 'ü' -> out.append('u') + 'ý' -> out.append('y') + 'ž', 'ź' -> out.append('z') + else -> if (ch.isLetter() || ch.isDigit()) out.append(ch) + } + } + return out.toString() +} + +private fun mSplit(raw: String): List? { + val s = raw.trim() + if (s.isEmpty()) return null + val semi = s.contains(';') + val parts = ArrayList() + var rest = s + while (rest.isNotEmpty()) { + val idx = if (semi) rest.indexOf(';') else rest.indexOf(", ") + val step = if (semi) 1 else 2 + val piece: String + val next: String + if (idx < 0) { + piece = rest.trim() + next = "" + } else { + piece = rest.substring(0, idx).trim() + next = rest.substring(idx + step) + } + if (piece.isEmpty()) return null + parts.add(piece) + if (idx < 0) break + rest = next + } + return parts.ifEmpty { null } +} + +fun mathAnswerOk(spec: String?, user: String?): Boolean { + if (spec == null || user == null) return false + val bodySpec = spec.trim() + if (bodySpec.startsWith("word:")) { + val opts = bodySpec.removePrefix("word:").split('|').map { mFold(it) } + return opts.contains(mFold(user)) + } + if (bodySpec.startsWith("set:") || bodySpec.startsWith("pair:")) { + val ordered = bodySpec.startsWith("pair:") + val body = bodySpec.removePrefix(if (ordered) "pair:" else "set:") + val want = mSplit(body) ?: return false + val got = mSplit(user) ?: return false + if (want.size != got.size || want.isEmpty()) return false + val wt = want.map { mParseTerm(it) ?: return false } + val gt = got.map { mParseTerm(it) ?: return false } + if (ordered) return wt.indices.all { mClose(wt[it], gt[it]) } + val used = BooleanArray(gt.size) + for (w in wt) { + var hit = gt.indices.firstOrNull { !used[it] && mExact(w, gt[it]) } + if (hit == null) hit = gt.indices.firstOrNull { !used[it] && mClose(w, gt[it]) } + if (hit == null) return false + used[hit] = true + } + return true + } + if (!bodySpec.startsWith("num:")) return false + val tw = mParseTerm(bodySpec.removePrefix("num:")) ?: return false + val tg = mParseTerm(user) ?: return false + return mClose(tw, tg) +} + +fun mathIsDraw(spec: String?): Boolean { + if (spec == null) return false + return spec.startsWith("square:") || spec.startsWith("mid:") || spec.startsWith("right:") || + spec.startsWith("para:") || spec.startsWith("points:") +} + +fun mathDrawNeed(spec: String?): Int { + if (spec == null) return 0 + if (spec.startsWith("square:")) return 2 + if (spec.startsWith("mid:") || spec.startsWith("right:") || spec.startsWith("para:")) return 1 + if (spec.startsWith("points:")) { + return spec.removePrefix("points:").split(';').count { !it.startsWith("fix:") } + } + return 0 +} + +private fun mParseXY(s: String): Pair? { + val bits = s.trim().split(',') + if (bits.size != 2) return null + val x = bits[0].trim().toIntOrNull() ?: return null + val y = bits[1].trim().toIntOrNull() ?: return null + return x to y +} + +fun mathDrawGiven(spec: String?): List> { + if (spec == null) return emptyList() + if (spec.startsWith("points:")) { + return spec.removePrefix("points:").split(';').mapNotNull { part -> + if (!part.startsWith("fix:")) null else mParseXY(part.removePrefix("fix:")) + } + } + val body: String + val n: Int + when { + spec.startsWith("para:") -> { body = spec.removePrefix("para:"); n = 3 } + spec.startsWith("square:") -> { body = spec.removePrefix("square:"); n = 2 } + spec.startsWith("right:") -> { body = spec.removePrefix("right:"); n = 2 } + spec.startsWith("mid:") -> { body = spec.removePrefix("mid:"); n = 2 } + else -> return emptyList() + } + return body.split(';').take(n).mapNotNull { mParseXY(it) } +} + +private fun mOnBoard(p: Pair) = p.first in 0..10 && p.second in 0..8 + +fun mathDrawOk(spec: String?, pts: List>): Boolean { + if (spec == null || mathDrawNeed(spec) != pts.size) return false + if (pts.any { !mOnBoard(it) }) return false + if (pts.map { "${it.first},${it.second}" }.toSet().size != pts.size) return false + val g = mathDrawGiven(spec) + if (spec.startsWith("mid:")) { + if (g.size != 2 || (g[0].first + g[1].first) % 2 != 0 || (g[0].second + g[1].second) % 2 != 0) return false + return pts[0] == (g[0].first + g[1].first) / 2 to (g[0].second + g[1].second) / 2 + } + if (spec.startsWith("para:")) { + if (g.size != 3) return false + return pts[0] == g[1].first + g[2].first - g[0].first to g[1].second + g[2].second - g[0].second + } + if (spec.startsWith("right:")) { + if (g.size != 2) return false + val c = pts[0] + if (c == g[0] || c == g[1]) return false + val ux = g[0].first - c.first + val uy = g[0].second - c.second + val vx = g[1].first - c.first + val vy = g[1].second - c.second + return ux * vx + uy * vy == 0 && (ux != 0 || uy != 0) && (vx != 0 || vy != 0) + } + if (spec.startsWith("square:")) { + if (g.size != 2 || pts.size != 2) return false + val a = g[0] + val b = g[1] + val dx = b.first - a.first + val dy = b.second - a.second + if (dx == 0 && dy == 0) return false + val c1 = b.first - dy to b.second + dx + val d1 = a.first - dy to a.second + dx + val c2 = b.first + dy to b.second - dx + val d2 = a.first + dy to a.second - dx + fun same(p: Pair, q: Pair, r: Pair, s: Pair) = + (p == r && q == s) || (p == s && q == r) + if ((!mOnBoard(c1) || !mOnBoard(d1)) && (!mOnBoard(c2) || !mOnBoard(d2))) return false + return same(pts[0], pts[1], c1, d1) || same(pts[0], pts[1], c2, d2) + } + if (spec.startsWith("points:")) { + val want = spec.removePrefix("points:").split(';').mapNotNull { part -> + if (part.startsWith("fix:")) null else mParseXY(part) + } + if (want.size != pts.size) return false + val used = BooleanArray(pts.size) + for (w in want) { + val j = pts.indices.firstOrNull { !used[it] && pts[it] == w } ?: return false + used[j] = true + } + return true + } + return false +} diff --git a/android/app/src/main/java/org/maturita/maturita/data/Content.kt b/android/app/src/main/java/org/maturita/maturita/data/Content.kt index a496309..b28c5b4 100644 --- a/android/app/src/main/java/org/maturita/maturita/data/Content.kt +++ b/android/app/src/main/java/org/maturita/maturita/data/Content.kt @@ -43,6 +43,11 @@ class Content(root: JSONObject) { val fyz2 = raw.arr("fyz2") val fyz3 = raw.arr("fyz3") val fyz4 = raw.arr("fyz4") + val mat0 = raw.arr("mat0") + val mat = raw.arr("mat") + val mat2 = raw.arr("mat2") + val mat3 = raw.arr("mat3") + val mat4 = raw.arr("mat4") val mluvnice = raw.arr("mluvnice") val books = raw.arr("books") val changelog = raw.arr("changelog") @@ -64,6 +69,11 @@ class Content(root: JSONObject) { fun fyz2Lesson(id: Int) = fyz2.firstOrNull { it.int("id") == id } fun fyz3Lesson(id: Int) = fyz3.firstOrNull { it.int("id") == id } fun fyz4Lesson(id: Int) = fyz4.firstOrNull { it.int("id") == id } + fun mat0Lesson(id: Int) = mat0.firstOrNull { it.int("id") == id } + fun matLesson(id: Int) = mat.firstOrNull { it.int("id") == id } + fun mat2Lesson(id: Int) = mat2.firstOrNull { it.int("id") == id } + fun mat3Lesson(id: Int) = mat3.firstOrNull { it.int("id") == id } + fun mat4Lesson(id: Int) = mat4.firstOrNull { it.int("id") == id } fun mluv(n: Int) = mluvnice.firstOrNull { it.int("id") == n } fun book(id: String) = books.firstOrNull { it.str("id") == id } diff --git a/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt b/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt index d6daf2d..334094b 100644 --- a/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt +++ b/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt @@ -65,6 +65,16 @@ class ProgressStore(context: Context) { fun markFyz3(id: Int) { prefs.edit().putBoolean("fyz3.$id", true).apply() } fun fyz4Done(id: Int) = prefs.getBoolean("fyz4.$id", false) fun markFyz4(id: Int) { prefs.edit().putBoolean("fyz4.$id", true).apply() } + fun mat0Done(id: Int) = prefs.getBoolean("mat0.$id", false) + fun markMat0(id: Int) { prefs.edit().putBoolean("mat0.$id", true).apply() } + fun matDone(id: Int) = prefs.getBoolean("mat.$id", false) + fun markMat(id: Int) { prefs.edit().putBoolean("mat.$id", true).apply() } + fun mat2Done(id: Int) = prefs.getBoolean("mat2.$id", false) + fun markMat2(id: Int) { prefs.edit().putBoolean("mat2.$id", true).apply() } + fun mat3Done(id: Int) = prefs.getBoolean("mat3.$id", false) + fun markMat3(id: Int) { prefs.edit().putBoolean("mat3.$id", true).apply() } + fun mat4Done(id: Int) = prefs.getBoolean("mat4.$id", false) + fun markMat4(id: Int) { prefs.edit().putBoolean("mat4.$id", true).apply() } fun mluvDone(n: Int) = prefs.getBoolean("mluv.$n", false) fun markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() } @@ -216,6 +226,46 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum { sum.doneUnits += 1 } } + content.mat0.forEach { l -> + sum.totalEx += 1 + sum.openUnits += 1 + if (p.mat0Done(l.int("id"))) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + content.mat.forEach { l -> + sum.totalEx += 1 + sum.openUnits += 1 + if (p.matDone(l.int("id"))) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + content.mat2.forEach { l -> + sum.totalEx += 1 + sum.openUnits += 1 + if (p.mat2Done(l.int("id"))) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + content.mat3.forEach { l -> + sum.totalEx += 1 + sum.openUnits += 1 + if (p.mat3Done(l.int("id"))) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + content.mat4.forEach { l -> + sum.totalEx += 1 + sum.openUnits += 1 + if (p.mat4Done(l.int("id"))) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } sum.totalEx += content.mluvnice.size sum.openUnits += 1 val md = content.mluvnice.count { p.mluvDone(it.int("id")) } diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt b/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt index e6b1e2f..93d3010 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt @@ -1,6 +1,8 @@ package org.maturita.maturita.ui import androidx.compose.foundation.Canvas +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,6 +14,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn @@ -45,18 +48,28 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.maturita.maturita.AppViewModel import org.maturita.maturita.data.J import org.maturita.maturita.data.answerAccepts +import org.maturita.maturita.data.mathAnswerOk +import org.maturita.maturita.data.mathDrawGiven +import org.maturita.maturita.data.mathDrawNeed +import org.maturita.maturita.data.mathDrawOk +import org.maturita.maturita.data.mathIsDraw import org.maturita.maturita.data.answerIncomplete import org.maturita.maturita.data.hasUmlaut import org.maturita.maturita.data.netTxtEq @@ -863,6 +876,198 @@ fun SciQuizScreen(vm: AppViewModel, area: String, id: Int) { } } +@Composable +fun MathPracticeScreen(vm: AppViewModel, year: Int, id: Int) { + val lesson = when (year) { + 0 -> vm.content.mat0Lesson(id) + 2 -> vm.content.mat2Lesson(id) + 3 -> vm.content.mat3Lesson(id) + 4 -> vm.content.mat4Lesson(id) + else -> vm.content.matLesson(id) + } ?: return + val problems = lesson.arr("problems") + var texts by remember(year, id) { mutableStateOf(List(problems.size) { "" }) } + var placed by remember(year, id) { mutableStateOf(List(problems.size) { emptyList>() }) } + var marks by remember(year, id) { mutableStateOf(List(problems.size) { null }) } + var revealed by remember(year, id) { mutableStateOf(false) } + var feedback by remember(year, id) { mutableStateOf("") } + var kind by remember(year, id) { mutableStateOf("") } + ExerciseScaffold( + vm, + vm.tr(lesson.str("exTitleKey")), + vm.tr(lesson.str("quizHeadKey")), + vm.tr("check"), + { + var all = problems.isNotEmpty() + val next = MutableList(problems.size) { null } + problems.forEachIndexed { i, problem -> + val spec = problem.str("answer") + val good = if (mathIsDraw(spec)) mathDrawOk(spec, placed[i]) else mathAnswerOk(spec, texts[i]) + next[i] = good + if (!good) all = false + } + marks = next + revealed = true + if (all) { + feedback = vm.tr("feedback_ok") + kind = "ok" + when (year) { + 0 -> vm.markMat0(id) + 2 -> vm.markMat2(id) + 3 -> vm.markMat3(id) + 4 -> vm.markMat4(id) + else -> vm.markMat(id) + } + } else { + feedback = vm.tr("feedback_retry_short") + kind = "bad" + } + }, + feedback, + kind, + ) { + Text( + vm.tr("math_quiz_intro"), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(8.dp)) + problems.forEachIndexed { i, problem -> + val spec = problem.str("answer") + val border = when (marks.getOrNull(i)) { + true -> Color(0xFF2E7D32) + false -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.outlineVariant + } + Column( + Modifier + .fillMaxWidth() + .padding(bottom = 10.dp) + .border(1.5.dp, border, MaterialTheme.shapes.medium) + .padding(12.dp), + ) { + Text(problem.str("prompt"), style = MaterialTheme.typography.bodyLarge) + Spacer(Modifier.height(8.dp)) + if (mathIsDraw(spec)) { + Text( + vm.tr("math_draw_hint"), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(6.dp)) + MathBoard(mathDrawGiven(spec), placed[i], mathDrawNeed(spec), marks.getOrNull(i)) { next -> + placed = placed.toMutableList().also { it[i] = next } + } + Row { + OutlinedButton(onClick = { + val next = placed[i].dropLast(1) + placed = placed.toMutableList().also { it[i] = next } + }) { Text(vm.tr("math_undo")) } + Spacer(Modifier.width(8.dp)) + OutlinedButton(onClick = { + placed = placed.toMutableList().also { it[i] = emptyList() } + }) { Text(vm.tr("math_clear")) } + } + } else { + OutlinedTextField( + value = texts[i], + onValueChange = { value -> texts = texts.toMutableList().also { it[i] = value } }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text(vm.tr("math_answer_ph")) }, + singleLine = true, + ) + } + if (revealed) { + Spacer(Modifier.height(6.dp)) + Text( + problem.str("hint"), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } +} + +@Composable +private fun MathBoard( + given: List>, + user: List>, + need: Int, + mark: Boolean?, + onChange: (List>) -> Unit, +) { + val measurer = rememberTextMeasurer() + val accent = MaterialTheme.colorScheme.primary + val ink = MaterialTheme.colorScheme.onSurface + val grid = MaterialTheme.colorScheme.onSurfaceVariant + val paper = MaterialTheme.colorScheme.surfaceVariant + val ok = Color(0xFF2E7D32) + val bad = MaterialTheme.colorScheme.error + Canvas( + Modifier + .size(316.dp, 260.dp) + .pointerInput(given, user, need) { + detectTapGestures { pos -> + val cell = size.width.toFloat() / (10f + 36f / 28f) + val pad = cell * 18f / 28f + val cx = (pos.x - pad) / cell + val cy = (size.height.toFloat() - pad - pos.y) / cell + val ix = kotlin.math.round(cx) + val iy = kotlin.math.round(cy) + if (kotlin.math.abs(cx - ix) > 0.45f || kotlin.math.abs(cy - iy) > 0.45f) return@detectTapGestures + val x = ix.toInt() + val y = iy.toInt() + if (x !in 0..10 || y !in 0..8) return@detectTapGestures + val hit = x to y + if (given.any { it == hit }) return@detectTapGestures + val next = user.toMutableList() + val idx = next.indexOf(hit) + if (idx >= 0) next.removeAt(idx) + else if (next.size < need) next.add(hit) + else return@detectTapGestures + onChange(next) + } + }, + ) { + val cell = size.width / (10f + 36f / 28f) + val pad = cell * 18f / 28f + fun at(x: Int, y: Int) = Offset(pad + x * cell, size.height - pad - y * cell) + drawRect(paper) + for (i in 0..10) { + val x = pad + i * cell + drawLine(grid.copy(alpha = 0.55f), Offset(x, pad), Offset(x, size.height - pad), strokeWidth = 1f) + } + for (i in 0..8) { + val y = pad + i * cell + drawLine(grid.copy(alpha = 0.55f), Offset(pad, y), Offset(size.width - pad, y), strokeWidth = 1f) + } + drawLine(ink, Offset(pad, size.height - pad), Offset(size.width - pad, size.height - pad), strokeWidth = 2.4f) + drawLine(ink, Offset(pad, size.height - pad), Offset(pad, pad), strokeWidth = 2.4f) + if (given.size >= 2) { + drawLine(accent, at(given[0].first, given[0].second), at(given[1].first, given[1].second), strokeWidth = 3.5f) + if (given.size >= 3) { + drawLine(accent, at(given[0].first, given[0].second), at(given[2].first, given[2].second), strokeWidth = 3.5f) + } + } + val names = listOf("A", "B", "C", "D") + val label = TextStyle(color = ink, fontSize = 13.sp) + given.forEachIndexed { i, g -> + if (i >= names.size) return@forEachIndexed + val pt = at(g.first, g.second) + drawCircle(accent, radius = 7f, center = pt) + drawText(measurer, names[i], topLeft = Offset(pt.x + 8f, pt.y - 22f), style = label) + } + val dot = if (mark == false) bad else ok + user.forEachIndexed { i, u -> + val pt = at(u.first, u.second) + drawCircle(dot, radius = 7f, center = pt) + drawText(measurer, "${i + 1}", topLeft = Offset(pt.x + 8f, pt.y - 22f), style = label) + } + } +} + @Composable fun FyzQuizScreen(vm: AppViewModel, year: Int, id: Int) { val lesson = when (year) { diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt index c719dc2..ab84346 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt @@ -26,6 +26,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.MenuBook import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material.icons.filled.Functions import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.FormatListNumbered @@ -224,6 +225,7 @@ private fun SubjectIcon(icon: String, open: Boolean) { "cz" -> CzechFlag(28.dp) "flask" -> Icon(Icons.Filled.Science, null, tint = tint, modifier = Modifier.size(26.dp)) "bolt" -> Icon(Icons.Filled.Bolt, null, tint = tint, modifier = Modifier.size(26.dp)) + "function" -> Icon(Icons.Filled.Functions, null, tint = tint, modifier = Modifier.size(26.dp)) else -> Icon(Icons.Filled.Lock, null, tint = tint) } } @@ -316,6 +318,14 @@ private fun subjectSum(vm: AppViewModel, s: J): ProgressSum { sum.doneEx = vm.content.chem.count { vm.progress.chemDone(it.int("id")) } + vm.content.bio.count { vm.progress.bioDone(it.int("id")) } } + "matyears", "mat0map", "matmap", "mat2map", "mat3map", "mat4map" -> { + sum.totalEx = vm.content.mat0.size + vm.content.mat.size + vm.content.mat2.size + vm.content.mat3.size + vm.content.mat4.size + sum.doneEx = vm.content.mat0.count { vm.progress.mat0Done(it.int("id")) } + + vm.content.mat.count { vm.progress.matDone(it.int("id")) } + + vm.content.mat2.count { vm.progress.mat2Done(it.int("id")) } + + vm.content.mat3.count { vm.progress.mat3Done(it.int("id")) } + + vm.content.mat4.count { vm.progress.mat4Done(it.int("id")) } + } "fyzyears", "fyzmap", "fyz2map", "fyz3map", "fyz4map" -> { sum.totalEx = vm.content.fyz.size + vm.content.fyz2.size + vm.content.fyz3.size + vm.content.fyz4.size sum.doneEx = vm.content.fyz.count { vm.progress.fyzDone(it.int("id")) } + @@ -556,6 +566,32 @@ private fun buildSearch(vm: AppViewModel): List { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "lit4unit${l.int("id")}", extra = "literatura") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit4ex${l.int("id")}") } + add(vm.tr("Matematika"), vm.tr("search_lesson"), "matyears", extra = "matematika") + add(vm.tr("mat0_year"), vm.tr("search_lesson"), "mat0map", extra = "zakladka") + add(vm.tr("mat_year1"), vm.tr("search_lesson"), "matmap", extra = "rocnik") + add(vm.tr("mat_year2"), vm.tr("search_lesson"), "mat2map", extra = "rocnik") + add(vm.tr("mat_year3"), vm.tr("search_lesson"), "mat3map", extra = "rocnik") + add(vm.tr("mat_year4"), vm.tr("search_lesson"), "mat4map", extra = "rocnik") + vm.content.mat0.forEach { l -> + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat0unit${l.int("id")}", extra = "matematika") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat0ex${l.int("id")}") + } + vm.content.mat.forEach { l -> + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "matunit${l.int("id")}", extra = "matematika") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "matex${l.int("id")}") + } + vm.content.mat2.forEach { l -> + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat2unit${l.int("id")}", extra = "matematika") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat2ex${l.int("id")}") + } + vm.content.mat3.forEach { l -> + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat3unit${l.int("id")}", extra = "matematika") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat3ex${l.int("id")}") + } + vm.content.mat4.forEach { l -> + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat4unit${l.int("id")}", extra = "matematika") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat4ex${l.int("id")}") + } add(vm.tr("Fyzika"), vm.tr("search_lesson"), "fyzyears", extra = "fyzika") add(vm.tr("fyz_year1"), vm.tr("search_lesson"), "fyzmap", extra = "rocnik") add(vm.tr("fyz_year2"), vm.tr("search_lesson"), "fyz2map", extra = "rocnik") @@ -713,6 +749,21 @@ fun LitYearsScreen(vm: AppViewModel) { } } +@Composable +fun MatYearsScreen(vm: AppViewModel) { + DetailScaffold(vm, vm.tr("Matematika"), vm.tr("mat_years_sub")) { inner -> + LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) { + item { + NavRow(title = vm.tr("mat0_year"), subtitle = vm.tr("mat0_sub"), leading = { Text("ZŠ", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat0Map) } + NavRow(title = vm.tr("mat_year1"), subtitle = vm.tr("mat_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.MatMap) } + NavRow(title = vm.tr("mat_year2"), subtitle = vm.tr("mat2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat2Map) } + NavRow(title = vm.tr("mat_year3"), subtitle = vm.tr("mat3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat3Map) } + NavRow(title = vm.tr("mat_year4"), subtitle = vm.tr("mat4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat4Map) } + } + } + } +} + @Composable fun FyzYearsScreen(vm: AppViewModel) { DetailScaffold(vm, vm.tr("Fyzika"), vm.tr("fyz_years_sub")) { inner -> diff --git a/data/changelog.txt b/data/changelog.txt index 9357cc3..c809291 100644 --- a/data/changelog.txt +++ b/data/changelog.txt @@ -6,6 +6,12 @@ # cs: bullet # en: bullet +--- 2026-10-06 +cs: Matematika: opakování základní školy a čtyři ročníky, 40 lekcí +cs: Příklady se píšou a kontrolují, u geometrie se rýsuje na mřížku +en: Mathematics: elementary review and four years, 40 lessons +en: Examples are typed and checked, and some geometry is drawn on a grid + --- 2026-10-06 cs: Fyzika: čtyři ročníky, 32 lekcí podle osnov středních škol cs: Od mechaniky přes teplo a elektřinu až po optiku, jádro a astrofyziku diff --git a/ios/Graduately/AppModel.swift b/ios/Graduately/AppModel.swift index fe401e2..37beae3 100644 --- a/ios/Graduately/AppModel.swift +++ b/ios/Graduately/AppModel.swift @@ -198,6 +198,31 @@ final class AppModel: ObservableObject { refresh() } + func markMat0(_ id: Int) { + progress.markMat0(id) + refresh() + } + + func markMat(_ id: Int) { + progress.markMat(id) + refresh() + } + + func markMat2(_ id: Int) { + progress.markMat2(id) + refresh() + } + + func markMat3(_ id: Int) { + progress.markMat3(id) + refresh() + } + + func markMat4(_ id: Int) { + progress.markMat4(id) + refresh() + } + func markMluv(_ n: Int) { progress.markMluv(n) refresh() diff --git a/ios/Graduately/Data/Answers.swift b/ios/Graduately/Data/Answers.swift index b94dd7d..c9b5df9 100644 --- a/ios/Graduately/Data/Answers.swift +++ b/ios/Graduately/Data/Answers.swift @@ -58,3 +58,658 @@ func hasUmlaut(_ s: String?) -> Bool { guard let s else { return false } return s.contains(where: { "äöüÄÖÜß".contains($0) }) } + +// MARK: - Mathematics +// Same rules as src/math_check.c. Do not route numbers through normalizeAnswer. + +private struct MFrac { var n: Int64 = 0; var d: Int64 = 1 } +private struct MTerm { + var ok = true + var a = MFrac() + var b = MFrac() + var rad: Int64 = 0 + var p = MFrac() +} + +private func mGcd(_ a0: Int64, _ b0: Int64) -> Int64 { + var a = a0 < 0 ? -a0 : a0 + var b = b0 < 0 ? -b0 : b0 + while b != 0 { + let t = a % b + a = b + b = t + } + return a == 0 ? 1 : a +} + +private func mMul(_ a: Int64, _ b: Int64) -> Int64? { + let r = a.multipliedReportingOverflow(by: b) + return r.overflow ? nil : r.partialValue +} + +private func mSet(_ f: inout MFrac, _ n: Int64, _ d: Int64) -> Bool { + if d == 0 { return false } + var n = n, d = d + if d < 0 { n = -n; d = -d } + let g = mGcd(n, d) + f.n = n / g + f.d = d / g + return true +} + +private func mAddF(_ x: MFrac, _ y: MFrac) -> MFrac? { + guard let xn = mMul(x.n, y.d), let yn = mMul(y.n, x.d), + let d = mMul(x.d, y.d) else { return nil } + let s = xn.addingReportingOverflow(yn) + if s.overflow { return nil } + var o = MFrac() + return mSet(&o, s.partialValue, d) ? o : nil +} + +private func mMulF(_ x: MFrac, _ y: MFrac) -> MFrac? { + guard let n = mMul(x.n, y.n), let d = mMul(x.d, y.d) else { return nil } + var o = MFrac() + return mSet(&o, n, d) ? o : nil +} + +private func mDivF(_ x: MFrac, _ y: MFrac) -> MFrac? { + if y.n == 0 { return nil } + return mMulF(x, MFrac(n: y.d, d: y.n)) +} + +private func mSameF(_ x: MFrac, _ y: MFrac) -> Bool { x.n == y.n && x.d == y.d } + +private func mSqrtReduce(_ n0: Int64) -> (out: Int64, rad: Int64)? { + if n0 < 0 { return nil } + if n0 > 1_000_000 { return nil } + var n = n0 + var out: Int64 = 1 + var f: Int64 = 2 + while f * f <= n { + var c = 0 + while n % f == 0 { n /= f; c += 1 } + if c >= 2 { out *= mPow(f, c / 2) } + if c % 2 == 1 { n *= f } + if f == 2 { f = 3 } else { f += 2 } + if f > 10000 { break } + } + return (out, n) +} + +private func mPow(_ b: Int64, _ e: Int) -> Int64 { + var r: Int64 = 1 + var i = 0 + while i < e { r *= b; i += 1 } + return r +} + +private func mNorm(_ t: inout MTerm) { + if !t.ok { return } + if t.rad > 1, let s = mSqrtReduce(t.rad) { + guard let nb = mMulF(t.b, MFrac(n: s.out, d: 1)) else { t.ok = false; return } + t.b = nb + t.rad = s.rad + } + if t.rad <= 1 { + guard let na = mAddF(t.a, t.b) else { t.ok = false; return } + t.a = na + t.b = MFrac() + t.rad = 0 + } + if t.b.n == 0 { t.rad = 0 } +} + +private func mAdd(_ x: MTerm, _ y: MTerm) -> MTerm { + if !x.ok || !y.ok { return MTerm(ok: false) } + var o = MTerm() + guard let a = mAddF(x.a, y.a), let p = mAddF(x.p, y.p) else { return MTerm(ok: false) } + o.a = a + o.p = p + if x.rad == 0 && y.rad == 0 { + o.b = MFrac(); o.rad = 0 + } else if x.rad == 0 { + o.b = y.b; o.rad = y.rad + } else if y.rad == 0 || x.rad == y.rad { + guard let b = mAddF(x.b, y.b) else { return MTerm(ok: false) } + o.b = b; o.rad = x.rad + } else { return MTerm(ok: false) } + mNorm(&o) + return o +} + +private func mScale(_ x: MTerm, _ f: MFrac) -> MTerm { + if !x.ok { return MTerm(ok: false) } + var o = MTerm() + guard let a = mMulF(x.a, f), let b = mMulF(x.b, f), let p = mMulF(x.p, f) else { + return MTerm(ok: false) + } + o.a = a; o.b = b; o.rad = x.rad; o.p = p + mNorm(&o) + return o +} + +private func mMulT(_ x: MTerm, _ y: MTerm) -> MTerm { + if !x.ok || !y.ok { return MTerm(ok: false) } + if (x.p.n != 0 && (y.p.n != 0 || y.rad != 0)) || + (y.p.n != 0 && (x.p.n != 0 || x.rad != 0)) { return MTerm(ok: false) } + if x.rad != 0 && y.rad != 0 && x.rad != y.rad { return MTerm(ok: false) } + var o = MTerm() + guard let aa = mMulF(x.a, y.a) else { return MTerm(ok: false) } + o.a = aa + if x.rad == 0 && y.rad == 0 { + o.b = MFrac(); o.rad = 0 + } else if x.rad == 0 { + guard let b = mMulF(x.a, y.b) else { return MTerm(ok: false) } + o.b = b; o.rad = y.rad + } else if y.rad == 0 { + guard let b = mMulF(y.a, x.b) else { return MTerm(ok: false) } + o.b = b; o.rad = x.rad + } else { + guard let ab = mMulF(x.a, y.b), let ba = mMulF(x.b, y.a), + let cross = mAddF(ab, ba), + let sq = mMul(x.rad, y.rad) else { return MTerm(ok: false) } + guard let bb = mMulF(x.b, y.b), let extra = mMulF(bb, MFrac(n: sq, d: 1)), + let na = mAddF(o.a, extra) else { return MTerm(ok: false) } + o.a = na; o.b = cross; o.rad = x.rad + } + if x.p.n != 0 { o.p = x.p } else { o.p = y.p } + if y.a.n != 1 || y.a.d != 1 || y.b.n != 0 || y.rad != 0 || y.p.n != 0 { + if x.p.n != 0 { + guard let p = mMulF(x.p, y.a) else { return MTerm(ok: false) } + o.p = p + } + } + if x.a.n != 1 || x.a.d != 1 || x.b.n != 0 || x.rad != 0 || x.p.n != 0 { + if y.p.n != 0 && x.p.n == 0 { + guard let p = mMulF(y.p, x.a) else { return MTerm(ok: false) } + o.p = p + } + } + mNorm(&o) + return o +} + +private func mDivT(_ x: MTerm, _ y: MTerm) -> MTerm { + if !x.ok || !y.ok || y.p.n != 0 { return MTerm(ok: false) } + if y.rad == 0 { + guard let f = mDivF(MFrac(n: 1, d: 1), y.a) else { return MTerm(ok: false) } + return mScale(x, f) + } + if x.p.n != 0 { return MTerm(ok: false) } + guard let yb = mMulF(y.b, MFrac(n: y.rad, d: 1)), + let bb = mMulF(y.b, yb), + let aa = mMulF(y.a, y.a), + let den = mAddF(aa, MFrac(n: -bb.n, d: bb.d)) else { return MTerm(ok: false) } + var num = MTerm() + num.a = y.a + num.b = MFrac(n: -y.b.n, d: y.b.d) + num.rad = y.rad + guard let inv = mDivF(MFrac(n: 1, d: 1), den) else { return MTerm(ok: false) } + return mMulT(x, mScale(num, inv)) +} + +private enum MTok { + case num(Int64, Int64) + case sqrt, pi, add, sub, mul, div, lp, rp +} + +private func mLex(_ s: String) -> [MTok]? { + var toks: [MTok] = [] + var i = s.startIndex + func peek(_ k: Int) -> Character? { + var j = i + var n = 0 + while n < k && j < s.endIndex { j = s.index(after: j); n += 1 } + return j < s.endIndex ? s[j] : nil + } + while i < s.endIndex { + while i < s.endIndex && s[i].isWhitespace { i = s.index(after: i) } + if i >= s.endIndex { break } + let ch = s[i] + if ch.isNumber || ((ch == "." || ch == ",") && (peek(1)?.isNumber == true)) { + var whole: Int64 = 0 + var saw = false + while i < s.endIndex && s[i].isNumber { + saw = true + whole = whole * 10 + Int64(s[i].wholeNumberValue ?? 0) + i = s.index(after: i) + } + if i < s.endIndex && (s[i] == "." || s[i] == ",") && (s.index(after: i) < s.endIndex && s[s.index(after: i)].isNumber) { + i = s.index(after: i) + var frac: Int64 = 0 + var div: Int64 = 1 + while i < s.endIndex && s[i].isNumber { + frac = frac * 10 + Int64(s[i].wholeNumberValue ?? 0) + div *= 10 + i = s.index(after: i) + } + let n = whole * div + frac + toks.append(.num(n, div)) + } else if saw { + toks.append(.num(whole, 1)) + } else { return nil } + continue + } + if ch == "+" { toks.append(.add); i = s.index(after: i); continue } + if ch == "-" || ch == "−" || ch == "–" { toks.append(.sub); i = s.index(after: i); continue } + if ch == "*" || ch == "×" || ch == "·" { toks.append(.mul); i = s.index(after: i); continue } + if ch == "/" || ch == ":" { toks.append(.div); i = s.index(after: i); continue } + if ch == "(" { toks.append(.lp); i = s.index(after: i); continue } + if ch == ")" { toks.append(.rp); i = s.index(after: i); continue } + if ch == "π" { toks.append(.pi); i = s.index(after: i); continue } + if ch == "√" { toks.append(.sqrt); i = s.index(after: i); continue } + if ch.isLetter { + var j = i + while j < s.endIndex && s[j].isLetter { j = s.index(after: j) } + let w = s[i.. MTok? { i < t.count ? t[i] : nil } + mutating func take() -> MTok? { + if i >= t.count { return nil } + defer { i += 1 } + return t[i] + } + mutating func expr() -> MTerm? { + guard var left = term() else { return nil } + while let p = peek() { + if case .add = p { _ = take() } + else if case .sub = p { _ = take() } + else { break } + guard let r = term() else { return nil } + if case .sub = p { left = mAdd(left, mScale(r, MFrac(n: -1, d: 1))) } + else { left = mAdd(left, r) } + if !left.ok { return nil } + } + return left + } + mutating func term() -> MTerm? { + guard var left = unary() else { return nil } + while let p = peek() { + if case .mul = p { _ = take() } + else if case .div = p { _ = take() } + else { break } + guard let r = unary() else { return nil } + if case .div = p { left = mDivT(left, r) } + else { left = mMulT(left, r) } + if !left.ok { return nil } + } + return left + } + mutating func unary() -> MTerm? { + if case .sub = peek() { + _ = take() + guard let v = unary() else { return nil } + return mScale(v, MFrac(n: -1, d: 1)) + } + if case .add = peek() { + _ = take() + return unary() + } + return factor() + } + mutating func factor() -> MTerm? { + guard var left = primary() else { return nil } + while true { + guard let p = peek() else { break } + switch p { + case .sqrt, .pi, .lp: + guard let r = primary() else { return nil } + left = mMulT(left, r) + if !left.ok { return nil } + case .num: + return nil + default: + return left + } + } + return left + } + mutating func primary() -> MTerm? { + guard let p = take() else { return nil } + switch p { + case .num(let n, let d): + if case .div = peek(), case .num = t[safe: i + 1] { + _ = take() + if case .num(let n2, let d2) = take() { + if n2 <= 0 || d2 != 1 || d != 1 { return nil } + var o = MTerm() + if !mSet(&o.a, n, n2) { return nil } + return o + } + return nil + } + if d == 1, case .num(let n2, let d2) = peek(), n >= 0 { + _ = take() + if case .div = peek(), case .num(let n3, let d3) = t[safe: i + 1], d2 == 1, d3 == 1, n2 >= 0, n3 > 0 { + _ = take() + _ = take() + var frac = MFrac() + if !mSet(&frac, n2, n3) { return nil } + var whole = MTerm() + whole.a = MFrac(n: n, d: 1) + var part = MTerm() + part.a = frac + return mAdd(whole, part) + } + i -= 1 + } + var o = MTerm() + if !mSet(&o.a, n, d) { return nil } + return o + case .pi: + var o = MTerm() + o.p = MFrac(n: 1, d: 1) + return o + case .sqrt: + if case .lp = peek() { + _ = take() + guard let inner = expr(), case .rp = take() else { return nil } + return mSqrtTerm(inner) + } + if case .num(let n, let d) = peek(), d == 1, n >= 0 { + _ = take() + return mSqrtTerm(MTerm(a: MFrac(n: n, d: 1))) + } + return nil + case .lp: + guard let inner = expr(), case .rp = take() else { return nil } + return inner + default: + return nil + } + } +} + +private extension Array { + subscript(safe i: Int) -> Element? { indices.contains(i) ? self[i] : nil } +} + +private func mSqrtTerm(_ inner: MTerm) -> MTerm? { + if !inner.ok || inner.p.n != 0 || inner.rad != 0 || inner.b.n != 0 { return nil } + if inner.a.n < 0 || inner.a.d != 1 { return nil } + guard let s = mSqrtReduce(inner.a.n) else { return nil } + var o = MTerm() + o.b = MFrac(n: s.out, d: 1) + o.rad = s.rad + mNorm(&o) + return o.ok ? o : nil +} + +private func mPrep(_ raw: String) -> String { + var s = raw.trimmingCharacters(in: .whitespaces) + var again = true + while again { + again = false + if s.count >= 2 { + let pairs: [(Character, Character)] = [("(", ")"), ("[", "]"), ("{", "}")] + if let last = s.last, pairs.contains(where: { $0.0 == s.first && $0.1 == last }) { + s = String(s.dropFirst().dropLast()).trimmingCharacters(in: .whitespaces) + again = true + continue + } + } + } + s = mStripUnits(s).trimmingCharacters(in: .whitespaces) + return s +} + +private func mParseTerm(_ s: String) -> MTerm? { + let s = mPrep(s) + if s.isEmpty { return nil } + guard let toks = mLex(s) else { return nil } + if toks.isEmpty { return nil } + var p = MParse(t: toks) + guard let v = p.expr(), p.i == toks.count, v.ok else { return nil } + return v +} + +private func mExact(_ x: MTerm, _ y: MTerm) -> Bool { + x.ok && y.ok && mSameF(x.a, y.a) && mSameF(x.b, y.b) && x.rad == y.rad && mSameF(x.p, y.p) +} + +private func mDouble(_ t: MTerm) -> Double { + var v = Double(t.a.n) / Double(t.a.d) + Double(t.p.n) / Double(t.p.d) * Double.pi + if t.b.n != 0 && t.rad > 0 { + v += Double(t.b.n) / Double(t.b.d) * (Double(t.rad)).squareRoot() + } + return v +} + +private func mClose(_ x: MTerm, _ y: MTerm) -> Bool { + if !x.ok || !y.ok { return false } + if mExact(x, y) { return true } + if x.p.n != 0 || y.p.n != 0 { return false } + let diff = abs(mDouble(x) - mDouble(y)) + if x.b.n != 0 || y.b.n != 0 { return diff <= 0.001 } + return diff <= 0.0005 +} + +private func mFold(_ s: String) -> String { + var out = "" + for ch in s.lowercased() { + switch ch { + case "á", "à", "ä", "â": out.append("a") + case "č", "ć": out.append("c") + case "ď": out.append("d") + case "é", "ě", "ë", "ê": out.append("e") + case "í", "ï": out.append("i") + case "ň": out.append("n") + case "ó", "ö", "ô": out.append("o") + case "ř": out.append("r") + case "š", "ś": out.append("s") + case "ť": out.append("t") + case "ú", "ů", "ü": out.append("u") + case "ý": out.append("y") + case "ž", "ź": out.append("z") + default: + if ch.isLetter || ch.isNumber { out.append(ch) } + } + } + return out +} + +private func mStripUnits(_ raw: String) -> String { + var s = raw.trimmingCharacters(in: .whitespaces) + let units = ["procenta", "procent", "cm", "mm", "dm", "km", "m"] + var cut = true + while cut { + cut = false + while s.last?.isWhitespace == true { s.removeLast() } + if s.hasSuffix("°") || s.hasSuffix("%") || s.hasSuffix(".") { + s.removeLast() + cut = true + continue + } + for u in units { + guard s.count > u.count else { continue } + if s.lowercased().hasSuffix(u) { + let keep = s.dropLast(u.count) + if keep.last?.isLetter == true { continue } + s = String(keep) + cut = true + break + } + } + } + return s.trimmingCharacters(in: .whitespaces) +} + +private func mSplit(_ s: String) -> [String]? { + let s = s.trimmingCharacters(in: .whitespaces) + if s.isEmpty { return nil } + let semi = s.contains(";") + var parts: [String] = [] + var rest = Substring(s) + while !rest.isEmpty { + let end: Substring.Index + let step: Int + if semi { + if let i = rest.firstIndex(of: ";") { end = i; step = 1 } + else { end = rest.endIndex; step = 0 } + } else if let r = rest.range(of: ", ") { + end = r.lowerBound + step = 2 + } else { + end = rest.endIndex + step = 0 + } + let piece = rest[.. Bool { + guard var spec, let user else { return false } + spec = spec.trimmingCharacters(in: .whitespaces) + if spec.hasPrefix("word:") { + let opts = spec.dropFirst(5).split(separator: "|").map { mFold(String($0)) } + return opts.contains(mFold(user)) + } + if spec.hasPrefix("set:") || spec.hasPrefix("pair:") { + let ordered = spec.hasPrefix("pair:") + let body = String(spec.dropFirst(ordered ? 5 : 4)) + guard let want = mSplit(body), let got = mSplit(user) else { return false } + if want.count != got.count || want.isEmpty { return false } + var wt: [MTerm] = [] + var gt: [MTerm] = [] + for w in want { + guard let t = mParseTerm(w) else { return false } + wt.append(t) + } + for g in got { + guard let t = mParseTerm(g) else { return false } + gt.append(t) + } + if ordered { + for i in wt.indices where !mClose(wt[i], gt[i]) { return false } + return true + } + var used = Array(repeating: false, count: gt.count) + for w in wt { + var hit = gt.indices.first { !used[$0] && mExact(w, gt[$0]) } + if hit == nil { hit = gt.indices.first { !used[$0] && mClose(w, gt[$0]) } } + guard let hit else { return false } + used[hit] = true + } + return true + } + let body = spec.hasPrefix("num:") ? String(spec.dropFirst(4)) : spec + guard let tw = mParseTerm(body), let tg = mParseTerm(user) else { return false } + return mClose(tw, tg) +} + +func mathIsDraw(_ spec: String?) -> Bool { + guard let spec else { return false } + return spec.hasPrefix("square:") || spec.hasPrefix("mid:") || spec.hasPrefix("right:") + || spec.hasPrefix("para:") || spec.hasPrefix("points:") +} + +func mathDrawNeed(_ spec: String?) -> Int { + guard let spec else { return 0 } + if spec.hasPrefix("square:") { return 2 } + if spec.hasPrefix("mid:") || spec.hasPrefix("right:") || spec.hasPrefix("para:") { return 1 } + if spec.hasPrefix("points:") { + let body = spec.dropFirst(7) + return body.split(separator: ";").filter { !$0.hasPrefix("fix:") }.count + } + return 0 +} + +private func mParseXY(_ s: Substring) -> (Int, Int)? { + let t = s.trimmingCharacters(in: .whitespaces) + let bits = t.split(separator: ",") + guard bits.count == 2, let x = Int(bits[0].trimmingCharacters(in: .whitespaces)), + let y = Int(bits[1].trimmingCharacters(in: .whitespaces)) else { return nil } + return (x, y) +} + +func mathDrawGiven(_ spec: String?) -> [(Int, Int)] { + guard let spec else { return [] } + if spec.hasPrefix("points:") { + return spec.dropFirst(7).split(separator: ";").compactMap { part -> (Int, Int)? in + guard part.hasPrefix("fix:") else { return nil } + return mParseXY(part.dropFirst(4)) + } + } + let body: Substring + let n: Int + if spec.hasPrefix("para:") { body = spec.dropFirst(5); n = 3 } + else if spec.hasPrefix("square:") { body = spec.dropFirst(7); n = 2 } + else if spec.hasPrefix("right:") { body = spec.dropFirst(6); n = 2 } + else if spec.hasPrefix("mid:") { body = spec.dropFirst(4); n = 2 } + else { return [] } + return body.split(separator: ";").prefix(n).compactMap { mParseXY($0) } +} + +private func mOnBoard(_ p: (Int, Int)) -> Bool { + p.0 >= 0 && p.0 <= 10 && p.1 >= 0 && p.1 <= 8 +} + +func mathDrawOk(_ spec: String?, _ pts: [(Int, Int)]) -> Bool { + guard let spec, mathDrawNeed(spec) == pts.count else { return false } + if pts.contains(where: { !mOnBoard($0) }) { return false } + if Set(pts.map { "\($0.0),\($0.1)" }).count != pts.count { return false } + let g = mathDrawGiven(spec) + if spec.hasPrefix("mid:") { + guard g.count == 2, (g[0].0 + g[1].0) % 2 == 0, (g[0].1 + g[1].1) % 2 == 0 else { return false } + return pts[0] == ((g[0].0 + g[1].0) / 2, (g[0].1 + g[1].1) / 2) + } + if spec.hasPrefix("para:") { + guard g.count == 3 else { return false } + return pts[0] == (g[1].0 + g[2].0 - g[0].0, g[1].1 + g[2].1 - g[0].1) + } + if spec.hasPrefix("right:") { + guard g.count == 2 else { return false } + let c = pts[0] + if c == g[0] || c == g[1] { return false } + let ux = g[0].0 - c.0, uy = g[0].1 - c.1 + let vx = g[1].0 - c.0, vy = g[1].1 - c.1 + return ux * vx + uy * vy == 0 && (ux != 0 || uy != 0) && (vx != 0 || vy != 0) + } + if spec.hasPrefix("square:") { + guard g.count == 2, pts.count == 2 else { return false } + let a = g[0], b = g[1] + let dx = b.0 - a.0, dy = b.1 - a.1 + if dx == 0 && dy == 0 { return false } + let c1 = (b.0 - dy, b.1 + dx), d1 = (a.0 - dy, a.1 + dx) + let c2 = (b.0 + dy, b.1 - dx), d2 = (a.0 + dy, a.1 - dx) + func same(_ p: (Int, Int), _ q: (Int, Int), _ r: (Int, Int), _ s: (Int, Int)) -> Bool { + (p == r && q == s) || (p == s && q == r) + } + if !mOnBoard(c1) || !mOnBoard(d1) { + if !mOnBoard(c2) || !mOnBoard(d2) { return false } + } + return same(pts[0], pts[1], c1, d1) || same(pts[0], pts[1], c2, d2) + } + if spec.hasPrefix("points:") { + let want = spec.dropFirst(7).split(separator: ";").compactMap { part -> (Int, Int)? in + if part.hasPrefix("fix:") { return nil } + return mParseXY(part) + } + if want.count != pts.count { return false } + var used = Array(repeating: false, count: pts.count) + for w in want { + guard let j = pts.indices.first(where: { !used[$0] && pts[$0] == w }) else { return false } + used[j] = true + } + return true + } + return false +} diff --git a/ios/Graduately/Data/Content.swift b/ios/Graduately/Data/Content.swift index de3a541..3dc8db2 100644 --- a/ios/Graduately/Data/Content.swift +++ b/ios/Graduately/Data/Content.swift @@ -45,6 +45,11 @@ final class Content { let fyz2: [J] let fyz3: [J] let fyz4: [J] + let mat0: [J] + let mat: [J] + let mat2: [J] + let mat3: [J] + let mat4: [J] let mluvnice: [J] let books: [J] let changelog: [J] @@ -90,6 +95,11 @@ final class Content { fyz2 = raw.arr("fyz2") fyz3 = raw.arr("fyz3") fyz4 = raw.arr("fyz4") + mat0 = raw.arr("mat0") + mat = raw.arr("mat") + mat2 = raw.arr("mat2") + mat3 = raw.arr("mat3") + mat4 = raw.arr("mat4") mluvnice = raw.arr("mluvnice") books = raw.arr("books") changelog = raw.arr("changelog") @@ -112,6 +122,11 @@ final class Content { func fyz2Lesson(_ id: Int) -> J? { fyz2.first { $0.int("id") == id } } func fyz3Lesson(_ id: Int) -> J? { fyz3.first { $0.int("id") == id } } func fyz4Lesson(_ id: Int) -> J? { fyz4.first { $0.int("id") == id } } + func mat0Lesson(_ id: Int) -> J? { mat0.first { $0.int("id") == id } } + func matLesson(_ id: Int) -> J? { mat.first { $0.int("id") == id } } + func mat2Lesson(_ id: Int) -> J? { mat2.first { $0.int("id") == id } } + func mat3Lesson(_ id: Int) -> J? { mat3.first { $0.int("id") == id } } + func mat4Lesson(_ id: Int) -> J? { mat4.first { $0.int("id") == id } } func mluv(_ n: Int) -> J? { mluvnice.first { $0.int("id") == n } } func book(_ id: String) -> J? { books.first { $0.str("id") == id } } diff --git a/ios/Graduately/Data/ProgressStore.swift b/ios/Graduately/Data/ProgressStore.swift index 22c4707..14a0b68 100644 --- a/ios/Graduately/Data/ProgressStore.swift +++ b/ios/Graduately/Data/ProgressStore.swift @@ -67,6 +67,16 @@ final class ProgressStore { func markFyz3(_ id: Int) { defaults.set(true, forKey: "fyz3.\(id)") } func fyz4Done(_ id: Int) -> Bool { defaults.bool(forKey: "fyz4.\(id)") } func markFyz4(_ id: Int) { defaults.set(true, forKey: "fyz4.\(id)") } + func mat0Done(_ id: Int) -> Bool { defaults.bool(forKey: "mat0.\(id)") } + func markMat0(_ id: Int) { defaults.set(true, forKey: "mat0.\(id)") } + func matDone(_ id: Int) -> Bool { defaults.bool(forKey: "mat.\(id)") } + func markMat(_ id: Int) { defaults.set(true, forKey: "mat.\(id)") } + func mat2Done(_ id: Int) -> Bool { defaults.bool(forKey: "mat2.\(id)") } + func markMat2(_ id: Int) { defaults.set(true, forKey: "mat2.\(id)") } + func mat3Done(_ id: Int) -> Bool { defaults.bool(forKey: "mat3.\(id)") } + func markMat3(_ id: Int) { defaults.set(true, forKey: "mat3.\(id)") } + func mat4Done(_ id: Int) -> Bool { defaults.bool(forKey: "mat4.\(id)") } + func markMat4(_ id: Int) { defaults.set(true, forKey: "mat4.\(id)") } func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") } func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") } @@ -223,6 +233,46 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum { sum.doneUnits += 1 } } + for l in content.mat0 { + sum.totalEx += 1 + sum.openUnits += 1 + if p.mat0Done(l.int("id")) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + for l in content.mat { + sum.totalEx += 1 + sum.openUnits += 1 + if p.matDone(l.int("id")) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + for l in content.mat2 { + sum.totalEx += 1 + sum.openUnits += 1 + if p.mat2Done(l.int("id")) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + for l in content.mat3 { + sum.totalEx += 1 + sum.openUnits += 1 + if p.mat3Done(l.int("id")) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + for l in content.mat4 { + sum.totalEx += 1 + sum.openUnits += 1 + if p.mat4Done(l.int("id")) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } sum.totalEx += content.mluvnice.count sum.openUnits += 1 let md = content.mluvnice.filter { p.mluvDone($0.int("id")) }.count diff --git a/ios/Graduately/Route.swift b/ios/Graduately/Route.swift index 9e6e42a..797474f 100644 --- a/ios/Graduately/Route.swift +++ b/ios/Graduately/Route.swift @@ -56,6 +56,22 @@ enum Route: Hashable { case bioMap case bioLesson(Int) case bioEx(Int) + case matYears + case mat0Map + case mat0Lesson(Int) + case mat0Ex(Int) + case matMap + case matLesson(Int) + case matEx(Int) + case mat2Map + case mat2Lesson(Int) + case mat2Ex(Int) + case mat3Map + case mat3Lesson(Int) + case mat3Ex(Int) + case mat4Map + case mat4Lesson(Int) + case mat4Ex(Int) case fyzYears case fyzMap case fyzLesson(Int) @@ -127,6 +143,22 @@ enum Route: Hashable { case .bioMap: return "biomap" case .bioLesson(let id): return "biounit\(id)" case .bioEx(let id): return "bioex\(id)" + case .matYears: return "matyears" + case .mat0Map: return "mat0map" + case .mat0Lesson(let id): return "mat0unit\(id)" + case .mat0Ex(let id): return "mat0ex\(id)" + case .matMap: return "matmap" + case .matLesson(let id): return "matunit\(id)" + case .matEx(let id): return "matex\(id)" + case .mat2Map: return "mat2map" + case .mat2Lesson(let id): return "mat2unit\(id)" + case .mat2Ex(let id): return "mat2ex\(id)" + case .mat3Map: return "mat3map" + case .mat3Lesson(let id): return "mat3unit\(id)" + case .mat3Ex(let id): return "mat3ex\(id)" + case .mat4Map: return "mat4map" + case .mat4Lesson(let id): return "mat4unit\(id)" + case .mat4Ex(let id): return "mat4ex\(id)" case .fyzYears: return "fyzyears" case .fyzMap: return "fyzmap" case .fyzLesson(let id): return "fyzunit\(id)" @@ -254,6 +286,38 @@ enum Route: Hashable { return [.sciMap, .bioMap, .bioLesson(id)] case .bioEx(let id): return [.sciMap, .bioMap, .bioLesson(id), .bioEx(id)] + case .matYears: + return [.matYears] + case .mat0Map: + return [.matYears, .mat0Map] + case .mat0Lesson(let id): + return [.matYears, .mat0Map, .mat0Lesson(id)] + case .mat0Ex(let id): + return [.matYears, .mat0Map, .mat0Lesson(id), .mat0Ex(id)] + case .matMap: + return [.matYears, .matMap] + case .matLesson(let id): + return [.matYears, .matMap, .matLesson(id)] + case .matEx(let id): + return [.matYears, .matMap, .matLesson(id), .matEx(id)] + case .mat2Map: + return [.matYears, .mat2Map] + case .mat2Lesson(let id): + return [.matYears, .mat2Map, .mat2Lesson(id)] + case .mat2Ex(let id): + return [.matYears, .mat2Map, .mat2Lesson(id), .mat2Ex(id)] + case .mat3Map: + return [.matYears, .mat3Map] + case .mat3Lesson(let id): + return [.matYears, .mat3Map, .mat3Lesson(id)] + case .mat3Ex(let id): + return [.matYears, .mat3Map, .mat3Lesson(id), .mat3Ex(id)] + case .mat4Map: + return [.matYears, .mat4Map] + case .mat4Lesson(let id): + return [.matYears, .mat4Map, .mat4Lesson(id)] + case .mat4Ex(let id): + return [.matYears, .mat4Map, .mat4Lesson(id), .mat4Ex(id)] case .fyzYears: return [.fyzYears] case .fyzMap: @@ -321,6 +385,12 @@ func routeFromPage(_ page: String) -> Route? { case "scimap": return .sciMap case "chemmap": return .chemMap case "biomap": return .bioMap + case "matyears": return .matYears + case "mat0map": return .mat0Map + case "matmap": return .matMap + case "mat2map": return .mat2Map + case "mat3map": return .mat3Map + case "mat4map": return .mat4Map case "fyzyears": return .fyzYears case "fyzmap": return .fyzMap case "fyz2map": return .fyz2Map @@ -381,6 +451,36 @@ func routeFromPage(_ page: String) -> Route? { if let m = page.wholeMatch(of: /bioex(\d+)/), let n = Int(m.1) { return .bioEx(n) } + if let m = page.wholeMatch(of: /mat0unit(\d+)/), let n = Int(m.1) { + return .mat0Lesson(n) + } + if let m = page.wholeMatch(of: /mat0ex(\d+)/), let n = Int(m.1) { + return .mat0Ex(n) + } + if let m = page.wholeMatch(of: /mat4unit(\d+)/), let n = Int(m.1) { + return .mat4Lesson(n) + } + if let m = page.wholeMatch(of: /mat4ex(\d+)/), let n = Int(m.1) { + return .mat4Ex(n) + } + if let m = page.wholeMatch(of: /mat3unit(\d+)/), let n = Int(m.1) { + return .mat3Lesson(n) + } + if let m = page.wholeMatch(of: /mat3ex(\d+)/), let n = Int(m.1) { + return .mat3Ex(n) + } + if let m = page.wholeMatch(of: /mat2unit(\d+)/), let n = Int(m.1) { + return .mat2Lesson(n) + } + if let m = page.wholeMatch(of: /mat2ex(\d+)/), let n = Int(m.1) { + return .mat2Ex(n) + } + if let m = page.wholeMatch(of: /matunit(\d+)/), let n = Int(m.1) { + return .matLesson(n) + } + if let m = page.wholeMatch(of: /matex(\d+)/), let n = Int(m.1) { + return .matEx(n) + } if let m = page.wholeMatch(of: /fyz4unit(\d+)/), let n = Int(m.1) { return .fyz4Lesson(n) } diff --git a/ios/Graduately/UI/Components.swift b/ios/Graduately/UI/Components.swift index 236a2ac..e095224 100644 --- a/ios/Graduately/UI/Components.swift +++ b/ios/Graduately/UI/Components.swift @@ -609,6 +609,7 @@ func subjectSymbol(_ icon: String) -> String { case "people": return "person.2.fill" case "flask": return "flask.fill" case "bolt": return "bolt.fill" + case "function": return "function" default: return "lock.fill" } } diff --git a/ios/Graduately/UI/ExercisesMore.swift b/ios/Graduately/UI/ExercisesMore.swift index 2b1f3ce..ce75f15 100644 --- a/ios/Graduately/UI/ExercisesMore.swift +++ b/ios/Graduately/UI/ExercisesMore.swift @@ -646,6 +646,255 @@ struct SciQuizScreen: View { } } +private func mathLesson(_ vm: AppModel, year: Int, id: Int) -> J? { + switch year { + case 0: return vm.content.mat0Lesson(id) + case 2: return vm.content.mat2Lesson(id) + case 3: return vm.content.mat3Lesson(id) + case 4: return vm.content.mat4Lesson(id) + default: return vm.content.matLesson(id) + } +} + +private func markMathLesson(_ vm: AppModel, year: Int, id: Int) { + switch year { + case 0: vm.markMat0(id) + case 2: vm.markMat2(id) + case 3: vm.markMat3(id) + case 4: vm.markMat4(id) + default: vm.markMat(id) + } +} + +struct MathPracticeScreen: View { + @ObservedObject var vm: AppModel + let year: Int + let id: Int + @State private var text: [Int: String] = [:] + @State private var points: [Int: [(Int, Int)]] = [:] + @State private var marks: [Int: Bool] = [:] + @State private var revealed = false + @State private var feedback = "" + @State private var kind = "" + + var body: some View { + if let lesson = mathLesson(vm, year: year, id: id) { + let problems = lesson.arr("problems") + ExerciseScaffold( + vm: vm, + title: vm.tr(lesson.str("exTitleKey")), + sub: vm.tr(lesson.str("quizHeadKey")), + action: vm.tr("check"), + onAction: { check(problems) }, + feedback: feedback, + kind: kind + ) { + Text(vm.tr("math_quiz_intro")) + .font(.subheadline) + .foregroundStyle(.secondary) + .padding(.bottom, 4) + ForEach(problems.indices, id: \.self) { i in + mathCard(problems[i], index: i) + } + } + .onChange(of: year) { _, _ in reset() } + .onChange(of: id) { _, _ in reset() } + } + } + + private func reset() { + text = [:] + points = [:] + marks = [:] + revealed = false + feedback = "" + kind = "" + } + + private func check(_ problems: [J]) { + var all = !problems.isEmpty + var next: [Int: Bool] = [:] + for i in problems.indices { + let spec = problems[i].str("answer") + let good = mathIsDraw(spec) + ? mathDrawOk(spec, points[i] ?? []) + : mathAnswerOk(spec, text[i] ?? "") + next[i] = good + if !good { all = false } + } + marks = next + revealed = true + if all { + feedback = vm.tr("feedback_ok") + kind = "ok" + markMathLesson(vm, year: year, id: id) + } else { + feedback = vm.tr("feedback_retry_short") + kind = "bad" + } + } + + @ViewBuilder + private func mathCard(_ problem: J, index i: Int) -> some View { + let spec = problem.str("answer") + let p = vm.palette + let border: Color = { + guard let mark = marks[i] else { return p.surface2 } + return mark ? p.success : p.error + }() + VStack(alignment: .leading, spacing: 8) { + Text(problem.str("prompt")) + .frame(maxWidth: .infinity, alignment: .leading) + if mathIsDraw(spec) { + Text(vm.tr("math_draw_hint")) + .font(.caption) + .foregroundStyle(.secondary) + MathGrid( + given: mathDrawGiven(spec), + user: points[i] ?? [], + need: mathDrawNeed(spec), + mark: marks[i], + palette: p + ) { next in + points[i] = next + } + HStack { + Button(vm.tr("math_undo")) { + var pts = points[i] ?? [] + if !pts.isEmpty { + pts.removeLast() + points[i] = pts + } + } + Button(vm.tr("math_clear")) { points[i] = [] } + } + .buttonStyle(.bordered) + } else { + TextField(vm.tr("math_answer_ph"), text: Binding( + get: { text[i] ?? "" }, + set: { text[i] = $0 } + )) + .textFieldStyle(.roundedBorder) + #if os(iOS) + .textInputAutocapitalization(.never) + .keyboardType(.numbersAndPunctuation) + #endif + .autocorrectionDisabled() + } + if revealed { + Text(problem.str("hint")) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + .padding(12) + .background(p.surface0, in: RoundedRectangle(cornerRadius: 12)) + .overlay(RoundedRectangle(cornerRadius: 12).stroke(border, lineWidth: 1.5)) + } +} + +private struct MathGrid: View { + let given: [(Int, Int)] + let user: [(Int, Int)] + let need: Int + let mark: Bool? + let palette: Palette + var onChange: ([(Int, Int)]) -> Void + + private let cell: CGFloat = 28 + private let pad: CGFloat = 18 + private let gx = 10 + private let gy = 8 + private var width: CGFloat { pad * 2 + CGFloat(gx) * cell } + private var height: CGFloat { pad * 2 + CGFloat(gy) * cell } + + private func screen(_ x: Int, _ y: Int) -> CGPoint { + CGPoint(x: pad + CGFloat(x) * cell, y: height - pad - CGFloat(y) * cell) + } + + private func snap(_ loc: CGPoint) -> (Int, Int)? { + let cx = (loc.x - pad) / cell + let cy = (height - pad - loc.y) / cell + let ix = Int(cx.rounded()) + let iy = Int(cy.rounded()) + if ix < 0 || ix > gx || iy < 0 || iy > gy { return nil } + if abs(cx - CGFloat(ix)) > 0.45 || abs(cy - CGFloat(iy)) > 0.45 { return nil } + return (ix, iy) + } + + var body: some View { + Canvas { ctx, _ in + let rect = CGRect(x: 0, y: 0, width: width, height: height) + ctx.fill(Path(rect), with: .color(palette.surface1)) + var grid = Path() + for i in 0...gx { + let x = pad + CGFloat(i) * cell + grid.move(to: CGPoint(x: x, y: pad)) + grid.addLine(to: CGPoint(x: x, y: height - pad)) + } + for i in 0...gy { + let y = pad + CGFloat(i) * cell + grid.move(to: CGPoint(x: pad, y: y)) + grid.addLine(to: CGPoint(x: width - pad, y: y)) + } + ctx.stroke(grid, with: .color(palette.subtext.opacity(0.55)), lineWidth: 1) + var axes = Path() + axes.move(to: CGPoint(x: pad, y: height - pad)) + axes.addLine(to: CGPoint(x: width - pad, y: height - pad)) + axes.move(to: CGPoint(x: pad, y: height - pad)) + axes.addLine(to: CGPoint(x: pad, y: pad)) + ctx.stroke(axes, with: .color(palette.text), lineWidth: 1.6) + if given.count >= 2 { + var seg = Path() + seg.move(to: screen(given[0].0, given[0].1)) + seg.addLine(to: screen(given[1].0, given[1].1)) + if given.count >= 3 { + seg.move(to: screen(given[0].0, given[0].1)) + seg.addLine(to: screen(given[2].0, given[2].1)) + } + ctx.stroke(seg, with: .color(palette.accent), lineWidth: 2) + } + let names = ["A", "B", "C", "D"] + for (i, g) in given.enumerated() where i < names.count { + let pt = screen(g.0, g.1) + ctx.fill(Path(ellipseIn: CGRect(x: pt.x - 5.5, y: pt.y - 5.5, width: 11, height: 11)), with: .color(palette.accent)) + ctx.draw( + Text(names[i]).font(.system(size: 13, weight: .semibold)).foregroundStyle(palette.text), + at: CGPoint(x: pt.x + 8, y: pt.y - 8), + anchor: .bottomLeading + ) + } + let dot = mark == false ? palette.error : palette.success + for (i, u) in user.enumerated() { + let pt = screen(u.0, u.1) + ctx.fill(Path(ellipseIn: CGRect(x: pt.x - 5.5, y: pt.y - 5.5, width: 11, height: 11)), with: .color(dot)) + ctx.draw( + Text("\(i + 1)").font(.system(size: 12, weight: .bold)).foregroundStyle(palette.text), + at: CGPoint(x: pt.x + 8, y: pt.y - 8), + anchor: .bottomLeading + ) + } + } + .frame(width: width, height: height) + .contentShape(Rectangle()) + .gesture( + DragGesture(minimumDistance: 0).onEnded { value in + guard let hit = snap(value.location) else { return } + if given.contains(where: { $0.0 == hit.0 && $0.1 == hit.1 }) { return } + var next = user + if let idx = next.firstIndex(where: { $0.0 == hit.0 && $0.1 == hit.1 }) { + next.remove(at: idx) + } else if next.count < need { + next.append(hit) + } else { + return + } + onChange(next) + } + ) + } +} + struct FyzQuizScreen: View { @ObservedObject var vm: AppModel let year: Int diff --git a/ios/Graduately/UI/Screens.swift b/ios/Graduately/UI/Screens.swift index 562786f..2696012 100644 --- a/ios/Graduately/UI/Screens.swift +++ b/ios/Graduately/UI/Screens.swift @@ -699,6 +699,13 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum { sum.totalEx = vm.content.chem.count + vm.content.bio.count sum.doneEx = vm.content.chem.filter { vm.progress.chemDone($0.int("id")) }.count + vm.content.bio.filter { vm.progress.bioDone($0.int("id")) }.count + case "matyears", "mat0map", "matmap", "mat2map", "mat3map", "mat4map": + sum.totalEx = vm.content.mat0.count + vm.content.mat.count + vm.content.mat2.count + vm.content.mat3.count + vm.content.mat4.count + sum.doneEx = vm.content.mat0.filter { vm.progress.mat0Done($0.int("id")) }.count + + vm.content.mat.filter { vm.progress.matDone($0.int("id")) }.count + + vm.content.mat2.filter { vm.progress.mat2Done($0.int("id")) }.count + + vm.content.mat3.filter { vm.progress.mat3Done($0.int("id")) }.count + + vm.content.mat4.filter { vm.progress.mat4Done($0.int("id")) }.count case "fyzyears", "fyzmap", "fyz2map", "fyz3map", "fyz4map": sum.totalEx = vm.content.fyz.count + vm.content.fyz2.count + vm.content.fyz3.count + vm.content.fyz4.count sum.doneEx = vm.content.fyz.filter { vm.progress.fyzDone($0.int("id")) }.count @@ -935,6 +942,32 @@ private func buildSearch(_ vm: AppModel) -> [Hit] { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "lit4unit\(l.int("id"))", extra: "literatura") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit4ex\(l.int("id"))") } + add(vm.tr("Matematika"), vm.tr("search_lesson"), "matyears", extra: "matematika") + add(vm.tr("mat0_year"), vm.tr("search_lesson"), "mat0map", extra: "zakladka") + add(vm.tr("mat_year1"), vm.tr("search_lesson"), "matmap", extra: "rocnik") + add(vm.tr("mat_year2"), vm.tr("search_lesson"), "mat2map", extra: "rocnik") + add(vm.tr("mat_year3"), vm.tr("search_lesson"), "mat3map", extra: "rocnik") + add(vm.tr("mat_year4"), vm.tr("search_lesson"), "mat4map", extra: "rocnik") + for l in vm.content.mat0 { + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat0unit\(l.int("id"))", extra: "matematika") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat0ex\(l.int("id"))") + } + for l in vm.content.mat { + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "matunit\(l.int("id"))", extra: "matematika") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "matex\(l.int("id"))") + } + for l in vm.content.mat2 { + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat2unit\(l.int("id"))", extra: "matematika") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat2ex\(l.int("id"))") + } + for l in vm.content.mat3 { + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat3unit\(l.int("id"))", extra: "matematika") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat3ex\(l.int("id"))") + } + for l in vm.content.mat4 { + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat4unit\(l.int("id"))", extra: "matematika") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat4ex\(l.int("id"))") + } add(vm.tr("Fyzika"), vm.tr("search_lesson"), "fyzyears", extra: "fyzika") add(vm.tr("fyz_year1"), vm.tr("search_lesson"), "fyzmap", extra: "rocnik") add(vm.tr("fyz_year2"), vm.tr("search_lesson"), "fyz2map", extra: "rocnik") @@ -1512,6 +1545,51 @@ struct LitYearsScreen: View { } } +struct MatYearsScreen: View { + @ObservedObject var vm: AppModel + + var body: some View { + #if os(macOS) + let years: [(String, String, Route, String)] = [ + ("mat0_year", "mat0_sub", .mat0Map, "ZŠ"), + ("mat_year1", "mat_sub", .matMap, "1."), + ("mat_year2", "mat2_sub", .mat2Map, "2."), + ("mat_year3", "mat3_sub", .mat3Map, "3."), + ("mat_year4", "mat4_sub", .mat4Map, "4."), + ] + MacFillGrid(count: years.count, minWidth: 260, minHeight: 180) { i in + NavigationLink(value: years[i].2) { + MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: years[i].3) + } + .buttonStyle(.plain) + } + .navigationTitle(vm.tr("Matematika")) + .navigationSubtitle(vm.tr("mat_years_sub")) + #else + List { + NavigationLink(value: Route.mat0Map) { + ListRowLabel(title: vm.tr("mat0_year"), subtitle: vm.tr("mat0_sub")) + } + NavigationLink(value: Route.matMap) { + ListRowLabel(title: vm.tr("mat_year1"), subtitle: vm.tr("mat_sub")) + } + NavigationLink(value: Route.mat2Map) { + ListRowLabel(title: vm.tr("mat_year2"), subtitle: vm.tr("mat2_sub")) + } + NavigationLink(value: Route.mat3Map) { + ListRowLabel(title: vm.tr("mat_year3"), subtitle: vm.tr("mat3_sub")) + } + NavigationLink(value: Route.mat4Map) { + ListRowLabel(title: vm.tr("mat_year4"), subtitle: vm.tr("mat4_sub")) + } + } + .navigationTitle(vm.tr("Matematika")) + .navigationSubtitle(vm.tr("mat_years_sub")) + .appListStyle() + #endif + } +} + struct FyzYearsScreen: View { @ObservedObject var vm: AppModel @@ -2321,6 +2399,88 @@ struct RouteDestination: View { } case .bioEx(let id): SciQuizScreen(vm: vm, area: "bio", id: id) + case .matYears: + MatYearsScreen(vm: vm) + case .mat0Map: + LessonListScreen( + vm: vm, + title: vm.tr("mat0_year"), + subtitle: vm.tr("mat0_sub"), + rows: vm.content.mat0.map { l in + let id = l.int("id") + return LessonRow(id: "m0\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat0Done(id), destination: .mat0Lesson(id)) + } + ) + case .mat0Lesson(let id): + if let l = vm.content.mat0Lesson(id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat0Ex(id)) + } + case .mat0Ex(let id): + MathPracticeScreen(vm: vm, year: 0, id: id) + case .matMap: + LessonListScreen( + vm: vm, + title: vm.tr("mat_year1"), + subtitle: vm.tr("mat_sub"), + rows: vm.content.mat.map { l in + let id = l.int("id") + return LessonRow(id: "m1\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.matDone(id), destination: .matLesson(id)) + } + ) + case .matLesson(let id): + if let l = vm.content.matLesson(id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .matEx(id)) + } + case .matEx(let id): + MathPracticeScreen(vm: vm, year: 1, id: id) + case .mat2Map: + LessonListScreen( + vm: vm, + title: vm.tr("mat_year2"), + subtitle: vm.tr("mat2_sub"), + rows: vm.content.mat2.map { l in + let id = l.int("id") + return LessonRow(id: "m2\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat2Done(id), destination: .mat2Lesson(id)) + } + ) + case .mat2Lesson(let id): + if let l = vm.content.mat2Lesson(id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat2Ex(id)) + } + case .mat2Ex(let id): + MathPracticeScreen(vm: vm, year: 2, id: id) + case .mat3Map: + LessonListScreen( + vm: vm, + title: vm.tr("mat_year3"), + subtitle: vm.tr("mat3_sub"), + rows: vm.content.mat3.map { l in + let id = l.int("id") + return LessonRow(id: "m3\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat3Done(id), destination: .mat3Lesson(id)) + } + ) + case .mat3Lesson(let id): + if let l = vm.content.mat3Lesson(id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat3Ex(id)) + } + case .mat3Ex(let id): + MathPracticeScreen(vm: vm, year: 3, id: id) + case .mat4Map: + LessonListScreen( + vm: vm, + title: vm.tr("mat_year4"), + subtitle: vm.tr("mat4_sub"), + rows: vm.content.mat4.map { l in + let id = l.int("id") + return LessonRow(id: "m4\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat4Done(id), destination: .mat4Lesson(id)) + } + ) + case .mat4Lesson(let id): + if let l = vm.content.mat4Lesson(id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat4Ex(id)) + } + case .mat4Ex(let id): + MathPracticeScreen(vm: vm, year: 4, id: id) case .fyzYears: FyzYearsScreen(vm: vm) case .fyzMap: diff --git a/scripts/extract-android-content.py b/scripts/extract-android-content.py index a55ab8b..74d17ec 100644 --- a/scripts/extract-android-content.py +++ b/scripts/extract-android-content.py @@ -455,6 +455,20 @@ def profile_items(rows) -> list[dict]: return out +def math_items(rows) -> list[dict]: + out = [] + for row in as_list(rows): + row = as_list(row) + if len(row) < 3: + continue + out.append({ + "prompt": as_str(row[0]) or "", + "answer": as_str(row[1]) or "", + "hint": as_str(row[2]) or "", + }) + return out + + def slides_items(rows) -> list[dict]: out = [] for row in as_list(rows): @@ -1116,6 +1130,28 @@ def main() -> int: fyz3_lessons = lit_pack("fyz3") fyz4_lessons = lit_pack("fyz4") + def math_pack(prefix): + lessons = [] + for i in range(1, 9): + fn_u = f"build_{prefix}_unit{i}_page" + fn_e = f"build_{prefix}_unit{i}_exercise_page" + lessons.append({ + "id": i, + "titleKey": f"{prefix}_unit{i}", + "subKey": f"{prefix}_unit{i}_sub", + "exTitleKey": f"{prefix}_ex{i}_title", + "quizHeadKey": f"{prefix}_quiz{i}_head", + "slides": slides_items(A(f"{fn_u}.slides")), + "problems": math_items(A(f"{fn_e}.qs")), + }) + return lessons + + mat0_lessons = math_pack("mat0") + mat_lessons = math_pack("mat") + mat2_lessons = math_pack("mat2") + mat3_lessons = math_pack("mat3") + mat4_lessons = math_pack("mat4") + on_lessons = [] for i in range(1, 18): on_lessons.append({ @@ -1563,7 +1599,7 @@ def main() -> int: {"key": "Český jazyk a literatura", "open": True, "target": "czechmap", "icon": "cz"}, {"key": "Občanská nauka", "open": True, "target": "onyears", "icon": "people"}, {"key": "English", "open": False, "target": None, "icon": "lock"}, - {"key": "Matematika", "open": False, "target": None, "icon": "lock"}, + {"key": "Matematika", "open": True, "target": "matyears", "icon": "function"}, {"key": "Fyzika", "open": True, "target": "fyzyears", "icon": "bolt"}, {"key": "Základy Přírodopisných věd", "open": True, "target": "scimap", "icon": "flask"}, {"key": "Technická grafika", "open": False, "target": None, "icon": "lock"}, @@ -1593,6 +1629,11 @@ def main() -> int: "fyz2": fyz2_lessons, "fyz3": fyz3_lessons, "fyz4": fyz4_lessons, + "mat0": mat0_lessons, + "mat": mat_lessons, + "mat2": mat2_lessons, + "mat3": mat3_lessons, + "mat4": mat4_lessons, "on2": on2_lessons, "on3": on3_lessons, "on4": on4_lessons, @@ -1635,6 +1676,7 @@ def main() -> int: print(f" chem lessons: {sum(1 for L in chem_lessons if L['slides'])}") print(f" bio lessons: {sum(1 for L in bio_lessons if L['slides'])}") print(f" fyz lessons: {sum(1 for L in fyz_lessons + fyz2_lessons + fyz3_lessons + fyz4_lessons if L['slides'])}") + print(f" math lessons: {sum(1 for L in mat0_lessons + mat_lessons + mat2_lessons + mat3_lessons + mat4_lessons if L['slides'])}") print(f" on2 lessons: {sum(1 for L in on2_lessons if L['slides'])}") print(f" on3 lessons: {sum(1 for L in on3_lessons if L['slides'])}") print(f" on4 lessons: {sum(1 for L in on4_lessons if L['slides'])}") @@ -1705,6 +1747,17 @@ def main() -> int: missing.append(f"{label}{L['id']} quiz") if not all(q.get("expl") for q in L["quiz"]): missing.append(f"{label}{L['id']} expl") + for label, block in ( + ("mat0", mat0_lessons), ("mat", mat_lessons), + ("mat2", mat2_lessons), ("mat3", mat3_lessons), ("mat4", mat4_lessons), + ): + for L in block: + if len(L["slides"]) != 3: + missing.append(f"{label}{L['id']} slides") + if not L["problems"]: + missing.append(f"{label}{L['id']} problems") + if not all(p.get("prompt") and p.get("answer") and p.get("hint") for p in L["problems"]): + missing.append(f"{label}{L['id']} answer") for m in mluvnice: if m["type"] in ("typed", "reveal") and not m.get("items"): missing.append(f"mluv{m['id']}") diff --git a/scripts/gen-math.py b/scripts/gen-math.py new file mode 100644 index 0000000..c5d2e9a --- /dev/null +++ b/scripts/gen-math.py @@ -0,0 +1,2040 @@ +#!/usr/bin/env python3 +"""Generate the mathematics lessons. + +Numeric answers are built from fractions.Fraction, then checked by +src/math_check.c. Slide text is Czech, like the other subjects. +""" +from __future__ import annotations + +import subprocess +import sys +from fractions import Fraction +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "src" + + +def show(value) -> str: + fr = Fraction(value) + if fr.denominator == 1: + return str(fr.numerator) + return f"{fr.numerator}/{fr.denominator}" + + +class Prob: + def __init__(self, prompt: str, spec: str, hint: str, draw: str | None = None): + self.prompt = prompt + self.spec = spec + self.hint = hint + self.draw = draw + + +def N(prompt: str, value, how: str) -> Prob: + fr = Fraction(value) + return Prob(prompt, "num:" + show(fr), f"{how} Výsledek je {show(fr)}.") + + +def PI(prompt: str, coeff, how: str) -> Prob: + fr = Fraction(coeff) + if fr == 1: + spec = "num:pi" + shown = "π" + elif fr == -1: + spec = "num:-pi" + shown = "-π" + elif fr.denominator == 1: + spec = f"num:{fr.numerator}*pi" + shown = f"{fr.numerator}π" + else: + spec = f"num:{show(fr)}*pi" + shown = f"({show(fr)})π" + return Prob(prompt, spec, f"{how} Výsledek je {shown}.") + + +def SQRT(prompt: str, spec: str, how: str) -> Prob: + if not spec.startswith("num:"): + raise ValueError(spec) + return Prob(prompt, spec, how) + + +def PAIR(prompt: str, a, b, how: str) -> Prob: + return Prob(prompt, f"pair:{show(a)};{show(b)}", + f"{how} Napište {show(a)};{show(b)}.") + + +def SET(prompt: str, values, how: str) -> Prob: + body = ";".join(show(v) for v in values) + return Prob(prompt, "set:" + body, f"{how} Napište {body}.") + + +def DRAW(prompt: str, spec: str, how: str, sample: str) -> Prob: + return Prob(prompt, spec, how, draw=sample) + + +def c_escape(text: str) -> str: + return text.replace("\\", "\\\\").replace('"', '\\"') + + +def c_string(text: str) -> str: + if "\n" in text: + raise ValueError(text) + return '"' + c_escape(text) + '"' + + +def lesson(title_cs, title_en, sub_cs, sub_en, slides, problems): + return { + "title_cs": title_cs, + "title_en": title_en, + "sub_cs": sub_cs, + "sub_en": sub_en, + "slides": slides, + "problems": problems, + } + + +def S(kicker, title, tip, lines): + if len(lines) < 3 or len(lines) > 7: + raise ValueError(title) + return (kicker, title, tip, lines) + + +def year0(): + return [ + lesson( + "Pořadí operací", "Order of operations", + "Závorky, násobení a dělení, sčítání a odčítání.", + "Brackets, then multiplication and division, then addition and subtraction.", + [ + S("1 / 3 • Pravidlo", "Co se počítá dřív", + "Tip: násobení a dělení jsou stejně silné a jdou zleva doprava.", + [ + "Nejprve se vyřeší závorky, zevnitř ven.", + "Pak jsou mocniny a odmocniny.", + "Násobení a dělení mají přednost před sčítáním a odčítáním.", + "Sčítání a odčítání se také provádějí zleva doprava.", + ]), + S("2 / 3 • Bez závorek", "Násobení má přednost", + "Tip: 2 + 3 · 4 není 20.", + [ + "Ve výrazu 2 + 3 · 4 se nejdřív násobí: 3 · 4 = 12.", + "Teprve potom se přičtou dvě: 2 + 12 = 14.", + "Ve výrazu 20 − 6 / 2 se nejdřív dělí: 6 / 2 = 3.", + "Pak 20 − 3 = 17.", + ]), + S("3 / 3 • Závorky", "Závorka změní pořadí", + "Tip: závorka se chová jako jeden počet.", + [ + "(8 − 3) · 2 začíná v závorce: 5 · 2 = 10.", + "3 · (4 + 1) je 3 · 5 = 15.", + "Bez závorky by 3 · 4 + 1 vyšlo 13.", + "Závorku proto pište vždy, když má sčítání předběhnout násobení.", + ]), + ], + [ + N("Spočítejte 2 + 3 · 4.", 2 + 3 * 4, + "Násobení má přednost: 3 · 4 = 12 a 2 + 12 = 14."), + N("Spočítejte (8 − 3) · 2.", (8 - 3) * 2, + "Nejdřív závorka: 8 − 3 = 5, potom 5 · 2 = 10."), + N("Spočítejte 20 − 6 / 2.", 20 - Fraction(6, 2), + "Nejdřív dělení: 6 / 2 = 3, potom 20 − 3 = 17."), + N("Spočítejte 3 · (4 + 1).", 3 * (4 + 1), + "Nejdřív závorka: 4 + 1 = 5, potom 3 · 5 = 15."), + ], + ), + lesson( + "Zlomky", "Fractions", + "Sčítání, odčítání, násobení a dělení zlomků.", + "Adding, subtracting, multiplying and dividing fractions.", + [ + S("1 / 3 • Součet", "Společný jmenovatel", + "Tip: zlomek v základním tvaru má nesoudělný čitatel a jmenovatel.", + [ + "Sčítat a odčítat lze zlomky se stejným jmenovatelem.", + "Jinak se jmenovatelé rozšíří na nejmenší společný násobek.", + "1/2 + 1/3 má společný jmenovatel 6: 3/6 + 2/6 = 5/6.", + "3/4 − 1/4 = 2/4 = 1/2.", + ]), + S("2 / 3 • Součin", "Čitatel s čitatelem", + "Tip: před násobením jde často krátit křížem.", + [ + "Zlomky se násobí čitatel čitatelem a jmenovatel jmenovatelem.", + "2/3 · 3/5 = 6/15 = 2/5.", + "Krátit se smí jen čitatel se jmenovatelem, ne dvě horní čísla mezi sebou.", + "Smíšené číslo se před počtem převede na zlomek.", + ]), + S("3 / 3 • Podíl", "Dělit znamená násobit převráceným", + "Tip: převrácený zlomek k a/b je b/a.", + [ + "Dělení zlomkem je násobení jeho převrácenou hodnotou.", + "(3/4) : (1/2) = 3/4 · 2/1 = 6/4 = 3/2.", + "Výsledek se uvede v základním tvaru.", + "Nulou se dělit nesmí, proto jmenovatel nesmí vyjít 0.", + ]), + ], + [ + N("Spočítejte 1/2 + 1/3. Zlomek v základním tvaru.", + Fraction(1, 2) + Fraction(1, 3), + "Společný jmenovatel je 6: 3/6 + 2/6 = 5/6."), + N("Spočítejte 3/4 − 1/4.", + Fraction(3, 4) - Fraction(1, 4), + "Jmenovatel už je stejný: 2/4 = 1/2."), + N("Spočítejte 2/3 · 3/5.", + Fraction(2, 3) * Fraction(3, 5), + "6/15 se zkrátí třemi na 2/5."), + N("Spočítejte (3/4) : (1/2).", + Fraction(3, 4) / Fraction(1, 2), + "Násobí se převráceným zlomkem: 3/4 · 2/1 = 3/2."), + ], + ), + lesson( + "Desetinná čísla", "Decimals", + "Sčítání, násobení a dělení desetinných čísel.", + "Adding, multiplying and dividing decimals.", + [ + S("1 / 3 • Zápis", "Čárka i tečka", + "Tip: 0,5 je totéž co 1/2.", + [ + "Desetinná čárka odděluje celou část od desetin, setin a tisícin.", + "0,1 je jedna desetina, 0,01 je jedna setina.", + "Při sčítání se čárky píšou pod sebe.", + "2,5 + 1,5 = 4.", + ]), + S("2 / 3 • Násobení", "Čárka se posouvá", + "Tip: násobení deseti posune čárku o jedno místo doprava.", + [ + "Počet desetinných míst součinu je součet počtů míst činitelů.", + "0,2 · 5 = 1, protože dvě desetiny pětkrát jsou jedna celá.", + "1,25 − 0,5 = 0,75, což je 3/4.", + "Výsledek lze psát desetinně i zlomkem.", + ]), + S("3 / 3 • Dělení", "Dělit desetinným číslem", + "Tip: dělitel se nejdřív upraví na celé číslo.", + [ + "3,6 : 0,3 se rozšíří deseti na 36 : 3 = 12.", + "Čárka se v děliteli i děleném posune o stejný počet míst.", + "Dělení deseti posune čárku o jedno místo doleva.", + "Nulou se ani u desetinných čísel dělit nesmí.", + ]), + ], + [ + N("Spočítejte 2,5 + 1,5.", Fraction("2.5") + Fraction("1.5"), + "Pět desetin a pět desetin je jedna celá, celkem 4."), + N("Spočítejte 0,2 · 5.", Fraction("0.2") * 5, + "Dvě desetiny pětkrát jsou 10 desetin, tedy 1."), + N("Spočítejte 3,6 : 0,3.", Fraction("3.6") / Fraction("0.3"), + "Posun čárky dá 36 : 3 = 12."), + N("Spočítejte 1,25 − 0,5.", Fraction("1.25") - Fraction("0.5"), + "1,25 − 0,50 = 0,75, tedy 3/4."), + ], + ), + lesson( + "Procenta", "Percentages", + "Procento ze základu, počet procent a cena po slevě.", + "A percentage of a base, the rate, and a price after a discount.", + [ + S("1 / 3 • Základ", "Jedno procento je setina", + "Tip: pište jen číslo, znak procenta ne.", + [ + "1 % ze základu je jedna setina základu.", + "p % ze základu z je (p/100) · z.", + "20 % ze 150 je 0,2 · 150 = 30.", + "Základ, procentová část a počet procent jsou tři veličiny.", + ]), + S("2 / 3 • Kolik procent", "Část dělená základem", + "Tip: výsledek 0,25 znamená 25 %.", + [ + "Počet procent je (část / základ) · 100.", + "15 ze 60 je 15/60 = 1/4, tedy 25 %.", + "Do odpovědi se píše 25, ne 0,25 a ne 25 %.", + "Zlomek se na procenta násobí stem.", + ]), + S("3 / 3 • Sleva a základ", "Dopočítání celku", + "Tip: sleva 10 % nechá z ceny 90 %.", + [ + "Po slevě 10 % zbývá 90 % původní ceny.", + "400 − 10 % je 0,9 · 400 = 360.", + "Když 20 % základu je 18, je 1 % rovno 18/20.", + "Celý základ je pak 100 takových dílů: 90.", + ]), + ], + [ + N("Kolik je 20 % ze 150? Napište jen číslo.", + Fraction(20, 100) * 150, + "20 % je 0,2 a 0,2 · 150 = 30."), + N("15 je kolik procent z 60? Napište jen číslo, bez znaku %.", + Fraction(15, 60) * 100, + "15/60 = 1/4 = 25 %."), + N("Cena 400 se sníží o 10 %. Jaká bude nová cena?", + 400 * Fraction(90, 100), + "Zůstane 90 %: 0,9 · 400 = 360."), + N("20 % základu je 18. Jak velký je základ?", + 18 / Fraction(20, 100), + "1 % je 18/20 = 0,9 a 100 % je 90."), + ], + ), + lesson( + "Poměr a měřítko", "Ratio and scale", + "Dělení v poměru, úměra a měřítko mapy.", + "Sharing in a ratio, proportion and the scale of a map.", + [ + S("1 / 3 • Poměr", "Díly celku", + "Tip: poměr 2 : 3 má dohromady 5 dílů.", + [ + "Poměr a : b říká, na kolik stejných dílů se celek rozdělí.", + "Počet dílů je a + b.", + "První část je a/(a + b) z celku.", + "Z 20 v poměru 2 : 3 připadá na první část 8.", + ]), + S("2 / 3 • Úměra", "Součin vnějších a vnitřních členů", + "Tip: 3 : 5 = x : 20 znamená 3/5 = x/20.", + [ + "V úměře a : b = c : d platí a · d = b · c.", + "Z 3/5 = x/20 vyjde x = 3/5 · 20 = 12.", + "Stejný poměr lze rozšiřovat i krátit.", + "Neznámý člen se dopočítá jedním násobením a jedním dělením.", + ]), + S("3 / 3 • Měřítko", "Mapa a skutečnost", + "Tip: 1 : 1000 znamená, že 1 cm na mapě je 10 m ve skutečnosti.", + [ + "Měřítko 1 : k zmenšuje: skutečnost je k-krát větší než plán.", + "5 cm na mapě 1 : 1000 je 5000 cm, tedy 50 m.", + "Měřítko 5 : 1 je zvětšení, obraz je pětkrát větší.", + "4 cm v měřítku 5 : 1 odpovídá obrazu 20 cm.", + ]), + ], + [ + N("Číslo 20 rozdělte v poměru 2 : 3. Jak velká je první část?", + 20 * Fraction(2, 5), + "Dílů je 5 a první část jsou 2 z nich: 2/5 · 20 = 8."), + N("Na mapě 1 : 1000 jsou 5 cm. Kolik metrů je to ve skutečnosti?", + Fraction(5 * 1000, 100), + "5 cm · 1000 = 5000 cm = 50 m."), + N("Platí 3 : 5 = x : 20. Kolik je x?", + Fraction(3, 5) * 20, + "x = 3/5 · 20 = 12."), + N("Úsečka 4 cm se zobrazí v měřítku 5 : 1. Jak dlouhý je obraz v centimetrech?", + 4 * 5, + "Měřítko 5 : 1 zvětšuje pětkrát: 4 · 5 = 20."), + ], + ), + lesson( + "Záporná čísla", "Negative numbers", + "Sčítání, násobení a absolutní hodnota.", + "Addition, multiplication and absolute value.", + [ + S("1 / 3 • Číselná osa", "Vpravo roste, vlevo klesá", + "Tip: přičíst kladné číslo znamená posun doprava.", + [ + "Kladná čísla leží vpravo od nuly, záporná vlevo.", + "−3 + 8 je posun z −3 o 8 kroků doprava, tedy na 5.", + "Odčítání záporného čísla je totéž co přičítání kladného.", + "6 − (−2) = 6 + 2 = 8.", + ]), + S("2 / 3 • Násobení", "Znaménka", + "Tip: dvě záporná znaménka dají kladný součin.", + [ + "Součin kladného a záporného čísla je záporný.", + "Součin dvou záporných čísel je kladný.", + "(−4) · (−5) = 20.", + "Stejné pravidlo platí pro dělení.", + ]), + S("3 / 3 • Absolutní hodnota", "Vzdálenost od nuly", + "Tip: absolutní hodnota nikdy není záporná.", + [ + "|a| je vzdálenost čísla a od nuly na ose.", + "|−7| = 7 a |7| = 7.", + "|0| = 0.", + "Absolutní hodnota smaže znaménko, ale číslo jinak nezmění.", + ]), + ], + [ + N("Spočítejte −3 + 8.", -3 + 8, + "Z −3 se jde o 8 doprava, na 5."), + N("Spočítejte (−4) · (−5).", (-4) * (-5), + "Součin dvou záporných čísel je kladný: 20."), + N("Spočítejte |−7|.", 7, + "Vzdálenost −7 od nuly je 7."), + N("Spočítejte 6 − (−2).", 6 - (-2), + "Odčítat −2 znamená přičíst 2: 6 + 2 = 8."), + ], + ), + lesson( + "Lineární rovnice", "Linear equations", + "Ekvivalentní úpravy a jednoduché rovnice.", + "Equivalent steps and simple equations.", + [ + S("1 / 3 • Rovnováha", "Stejně na obě strany", + "Tip: co uděláte vlevo, udělejte i vpravo.", + [ + "Rovnice říká, že levá strana se rovná pravé.", + "K oběma stranám lze přičíst stejné číslo.", + "Obě strany lze vynásobit stejným nenulovým číslem.", + "Cílem je osamostatnit neznámou.", + ]), + S("2 / 3 • Kroky", "Nejdřív číslo, pak koeficient", + "Tip: 2x − 4 = 10 se nejdřív zbaví čtyřky.", + [ + "x + 5 = 12 se odečtením pěti změní na x = 7.", + "3x = 15 se vydělí třemi: x = 5.", + "2x − 4 = 10 dá po přičtení čtyř 2x = 14, tedy x = 7.", + "Zkouška dosadí výsledek do původní rovnice.", + ]), + S("3 / 3 • Dělení neznámé", "Zlomek s x", + "Tip: x/2 = 6 se násobí dvěma.", + [ + "x/2 je totéž co (1/2) · x.", + "Násobení obou stran dvěma dá x = 12.", + "Násobit nulou se nesmí, rovnice by ztratila význam.", + "U lineární rovnice s nenulovým koeficientem je řešení jedno.", + ]), + ], + [ + N("Vyřešte x + 5 = 12. Napište x.", 12 - 5, + "Od obou stran se odečte 5."), + N("Vyřešte 3x = 15. Napište x.", Fraction(15, 3), + "Obě strany se vydělí 3."), + N("Vyřešte 2x − 4 = 10. Napište x.", Fraction(10 + 4, 2), + "2x = 14, takže x = 7."), + N("Vyřešte x/2 = 6. Napište x.", 6 * 2, + "Obě strany se násobí 2."), + ], + ), + lesson( + "Obvod, obsah a čtverec", "Perimeter, area and a square", + "Obdélník, trojúhelník a doplnění čtverce na mřížce.", + "A rectangle, a triangle, and completing a square on the grid.", + [ + S("1 / 3 • Obdélník", "Obvod a obsah", + "Tip: obvod je součet všech stran, obsah je součin dvou sousedních.", + [ + "Obdélník a × b má obvod 2 · (a + b).", + "Obdélník 5 × 3 má obvod 2 · 8 = 16.", + "Obsah je a · b. Obdélník 6 × 4 má obsah 24.", + "Jednotky obvodu jsou délkové, jednotky obsahu čtvereční.", + ]), + S("2 / 3 • Trojúhelník", "Základna a výška", + "Tip: výška je kolmá na základnu, ne délka šikmé strany.", + [ + "Součet vnitřních úhlů trojúhelníku je 180°.", + "Třetí úhel je 180° minus součet dvou známých.", + "Úhly 60° a 50° doplňuje 70°.", + "Obsah je (základna · výška) / 2. Pro 8 a 5 je to 20.", + ]), + S("3 / 3 • Čtverec", "Doplnění na mřížce", + "Tip: osa y míří nahoru. Klepnutím na bod ho odeberete.", + [ + "Čtverec má čtyři shodné strany a čtyři pravé úhly.", + "Druhé dva vrcholy vzniknou posunutím strany o stejnou délku kolmo.", + "U vodorovné strany leží zbývající vrcholy přímo nad ní, nebo pod ní.", + "Na mřížce jsou platné jen oba vrcholy téže strany.", + ]), + ], + [ + N("Obdélník má strany 5 a 3. Jaký je jeho obvod?", + 2 * (5 + 3), + "2 · (5 + 3) = 16."), + N("Obdélník má strany 6 a 4. Jaký je jeho obsah?", + 6 * 4, + "Obsah je 6 · 4 = 24."), + N("Trojúhelník má základnu 8 a výšku 5. Jaký je jeho obsah?", + Fraction(8 * 5, 2), + "Obsah je (8 · 5) / 2 = 20."), + N("Dva úhly trojúhelníku jsou 60° a 50°. Jak velký je třetí úhel ve stupních?", + 180 - 60 - 50, + "Součet je 180°, takže třetí úhel je 70°."), + DRAW("Na mřížce je strana čtverce od A = [2; 3] k B = [5; 3]. Doplňte zbývající dva vrcholy.", + "square:2,3;5,3", + "Strana má délku 3. Druhé dva vrcholy jsou [2; 6] a [5; 6], nebo [2; 0] a [5; 0].", + "5,6;2,6"), + ], + ), + ] + + +def year1(): + return [ + lesson( + "Dělitelnost a prvočísla", "Divisibility and primes", + "Největší společný dělitel, násobek a rozklad.", + "Highest common factor, lowest common multiple and factorisation.", + [ + S("1 / 3 • Dělitel", "Čísla, která dělí beze zbytku", + "Tip: prvočíslo má právě dva dělitele, jedničku a sebe.", + [ + "Dělitel čísla a je celé číslo, kterým a dělí beze zbytku.", + "Číslo 12 má dělitele 1, 2, 3, 4, 6 a 12, tedy šest dělitelů.", + "Prvočíslo nelze rozložit na součin dvou menších celých čísel větších než 1.", + "Jednička prvočíslo není.", + ]), + S("2 / 3 • NSD a NSN", "Společný dělitel a násobek", + "Tip: NSD · NSN dvou čísel je jejich součin.", + [ + "Největší společný dělitel je největší číslo, které dělí obě čísla.", + "NSD(24, 36) = 12.", + "Nejmenší společný násobek je nejmenší kladné číslo, které je násobkem obou.", + "NSN(6, 8) = 24.", + ]), + S("3 / 3 • Rozklad", "Součin mocnin prvočísel", + "Tip: společné prvočinitele berte v nejmenší mocnině pro NSD.", + [ + "Každé celé číslo větší než 1 má jediný rozklad na prvočísla.", + "72 = 2³ · 3².", + "V NSD se bere nižší exponent, v NSN vyšší.", + "Rozklad usnadní krácení zlomků i hledání společného jmenovatele.", + ]), + ], + [ + N("Jaký je největší společný dělitel čísel 24 a 36?", 12, + "Společní dělitelé jsou 1, 2, 3, 4, 6 a 12."), + N("Jaký je nejmenší společný násobek čísel 6 a 8?", 24, + "Násobky 6 jsou 6, 12, 18, 24, … a 24 je první z nich dělitelné osmi."), + N("Spočítejte 2³ · 3².", (2 ** 3) * (3 ** 2), + "8 · 9 = 72."), + N("Kolik kladných dělitelů má číslo 12?", 6, + "Jsou to 1, 2, 3, 4, 6 a 12."), + ], + ), + lesson( + "Mocniny a odmocniny", "Powers and roots", + "Mocniny se stejným základem a druhá odmocnina.", + "Powers with the same base and the square root.", + [ + S("1 / 3 • Mocnina", "Opakované násobení", + "Tip: aⁿ je a násobené sebou n-krát.", + [ + "2⁵ = 2 · 2 · 2 · 2 · 2 = 32.", + "10³ = 1000, tři nuly za jedničkou.", + "Každé číslo na první je ono samo a a⁰ = 1 pro a ≠ 0.", + "Záporný exponent znamená převrácenou hodnotu mocniny.", + ]), + S("2 / 3 • Pravidla", "Stejný základ", + "Tip: (aᵐ)ⁿ = aᵐ·ⁿ.", + [ + "aᵐ · aⁿ = aᵐ⁺ⁿ.", + "aᵐ : aⁿ = aᵐ⁻ⁿ pro a ≠ 0.", + "(a · b)ⁿ = aⁿ · bⁿ.", + "(2³)² = 2⁶ = 64.", + ]), + S("3 / 3 • Odmocnina", "Druhá odmocnina", + "Tip: √a je nezáporné číslo, jehož druhá mocnina je a.", + [ + "√81 = 9, protože 9² = 81.", + "√0 = 0 a √1 = 1.", + "Ze záporného čísla se v reálných číslech druhá odmocnina nedělá.", + "√(a²) = |a|, ne vždy a.", + ]), + ], + [ + N("Spočítejte 2⁵.", 2 ** 5, "Pět dvojkových činitelů je 32."), + N("Spočítejte 10³.", 10 ** 3, "10 · 10 · 10 = 1000."), + N("Spočítejte √81.", 9, "9 · 9 = 81."), + N("Spočítejte (2³)².", (2 ** 3) ** 2, "(8)² = 64, nebo 2⁶ = 64."), + ], + ), + lesson( + "Algebraické výrazy", "Algebraic expressions", + "Dosazení, druhá mocnina součtu a rozklad rozdílu čtverců.", + "Substitution, the square of a sum and a difference of squares.", + [ + S("1 / 3 • Dosazení", "Písmeno je číslo", + "Tip: nejdřív závorky, potom mocniny a násobení.", + [ + "Do výrazu se za proměnnou dosadí dané číslo.", + "2(x + 3) při x = 4 je 2 · 7 = 14.", + "3x − 2x + 5 se nejdřív sloučí na x + 5.", + "Při x = 2 je x + 5 = 7.", + ]), + S("2 / 3 • Vzorce", "Druhá mocnina dvojčlenu", + "Tip: (a + b)² není a² + b².", + [ + "(a + b)² = a² + 2ab + b².", + "(a − b)² = a² − 2ab + b².", + "a² − b² = (a − b)(a + b).", + "Pro a = 3 a b = 1 je (a + b)² = 16.", + ]), + S("3 / 3 • Zkrácení", "Rozdíl čtverců", + "Tip: krátit lze jen pro x, která nejsou kořenem jmenovatele.", + [ + "x² − 9 = (x − 3)(x + 3).", + "(x² − 9)/(x − 3) = x + 3 pro x ≠ 3.", + "Při x = 5 je hodnota 8.", + "Dosazení x = 3 by dělilo nulou, výraz tam není definován.", + ]), + ], + [ + N("Do výrazu 2(x + 3) dosažte x = 4.", 2 * (4 + 3), + "2 · (4 + 3) = 14."), + N("Do výrazu (a + b)² dosažte a = 3 a b = 1.", (3 + 1) ** 2, + "4² = 16. Rozepsáno 9 + 6 + 1 = 16."), + N("Do výrazu 3x − 2x + 5 dosažte x = 2.", 3 * 2 - 2 * 2 + 5, + "Výraz je x + 5, při x = 2 je 7."), + N("Do výrazu (x² − 9)/(x − 3) dosažte x = 5.", + Fraction(5 ** 2 - 9, 5 - 3), + "Pro x ≠ 3 je výraz roven x + 3, tedy 8."), + ], + ), + lesson( + "Lineární rovnice s úpravami", "Linear equations", + "Rovnice s koeficientem, závorkou a zlomkem.", + "Equations with a coefficient, a bracket and a fraction.", + [ + S("1 / 3 • Převod", "Neznámé na jednu stranu", + "Tip: člen s x se převádí s opačným znaménkem.", + [ + "2x + 1 = 9 se odečtením jedné změní na 2x = 8.", + "x = 4.", + "Členy s x patří obvykle vlevo, čísla vpravo.", + "Po každé úpravě zůstává rovnost zachovaná.", + ]), + S("2 / 3 • Obě strany", "Stejný člen vlevo i vpravo", + "Tip: 5x − 3 = 2x + 9 se zbaví 2x odečtením.", + [ + "5x − 2x = 9 + 3.", + "3x = 12 a x = 4.", + "Zkouška: levá strana 5 · 4 − 3 = 17, pravá 8 + 9 = 17.", + "Když koeficient u x vyjde 0, rovnice buď nemá řešení, nebo jich má nekonečně mnoho.", + ]), + S("3 / 3 • Závorka a zlomek", "Nejdřív roznásobit", + "Tip: x/2 + 1 = 5 se nejdřív zbaví jedničky.", + [ + "x/2 + 1 = 5 dá x/2 = 4, tedy x = 8.", + "3(x − 2) = 12 se vydělí třemi: x − 2 = 4.", + "Nebo se závorka roznásobí: 3x − 6 = 12.", + "Obě cesty dají x = 6.", + ]), + ], + [ + N("Vyřešte 2x + 1 = 9. Napište x.", Fraction(9 - 1, 2), + "2x = 8, takže x = 4."), + N("Vyřešte 5x − 3 = 2x + 9. Napište x.", Fraction(9 + 3, 5 - 2), + "3x = 12, takže x = 4."), + N("Vyřešte x/2 + 1 = 5. Napište x.", (5 - 1) * 2, + "x/2 = 4, takže x = 8."), + N("Vyřešte 3(x − 2) = 12. Napište x.", 12 / 3 + 2, + "x − 2 = 4, takže x = 6."), + ], + ), + lesson( + "Slovní úlohy", "Word problems", + "Dráha, společná práce, směs a sleva.", + "Distance, joint work, a mixture and a discount.", + [ + S("1 / 3 • Dráha", "Rychlost krát čas", + "Tip: jednotky času a rychlosti musí patřit k sobě.", + [ + "Při stálé rychlosti je dráha s = v · t.", + "60 km/h po dobu 2,5 h urazí 150 km.", + "2,5 h je dvě a půl hodiny, ne dvě hodiny a padesát minut.", + "Z dráhy a času se rychlost dopočítá jako s/t.", + ]), + S("2 / 3 • Práce a směs", "Sčítají se výkony, ne časy", + "Tip: kdo práci udělá za 3 hodiny, udělá za hodinu třetinu.", + [ + "První pracovník udělá za hodinu 1/6 práce, druhý 1/3.", + "Společně udělají 1/6 + 2/6 = 1/2 práce za hodinu, tedy celou za 2 hodiny.", + "U směsi se sčítá množství čisté látky.", + "2 kg dvacetiprocentní a 3 kg desetiprocentní směsi obsahují 0,4 + 0,3 = 0,7 kg, tedy 14 %.", + ]), + S("3 / 3 • Sleva", "Nová cena je část základu", + "Tip: po slevě 20 % zbývá 80 % původní ceny.", + [ + "Když je nová cena 160 a sleva byla 20 %, je 160 rovno 80 % původní ceny.", + "Původní cena je 160 / 0,8 = 200.", + "Sleva v korunách je pak 40.", + "Nepleťte si slevu 20 % s tím, že se k nové ceně přičte 20 %.", + ]), + ], + [ + N("Auto jede 2,5 hodiny rychlostí 60 km/h. Kolik kilometrů ujede?", + Fraction("2.5") * 60, + "s = v · t = 60 · 2,5 = 150."), + N("První pracovník práci udělá za 6 hodin, druhý za 3 hodiny. Za kolik hodin ji udělají společně?", + 1 / (Fraction(1, 6) + Fraction(1, 3)), + "Za hodinu spolu udělají 1/2 práce, celou tedy za 2 hodiny."), + N("Smíchají se 2 kg směsi s 20 % látky a 3 kg směsi s 10 % látky. Kolik procent látky má směs? Napište jen číslo.", + (2 * 20 + 3 * 10) / 5, + "Čisté látky je 0,7 kg z 5 kg, tedy 14 %."), + N("Po slevě 20 % stojí zboží 160. Jaká byla původní cena?", + 160 / Fraction(80, 100), + "160 je 80 % původní ceny, základ je 200."), + ], + ), + lesson( + "Planimetrie", "Plane figures", + "Úhly trojúhelníku, vnější úhel a pravidelný mnohoúhelník.", + "Angles of a triangle, the exterior angle and a regular polygon.", + [ + S("1 / 3 • Trojúhelník", "Součet 180°", + "Tip: rovnoramenný trojúhelník má dvě ramena a dva úhly u základny shodné.", + [ + "Součet vnitřních úhlů každého trojúhelníku je 180°.", + "V rovnoramenném trojúhelníku jsou úhly při základně shodné.", + "Když jsou úhly při základně 70°, je úhel proti základně 40°.", + "Rovnostranný trojúhelník má všechny strany shodné a každý úhel 60°.", + ]), + S("2 / 3 • Vnější úhel", "Součet dvou vzdálených vnitřních", + "Tip: vnější úhel a sousední vnitřní úhel dávají 180°.", + [ + "Vnější úhel vznikne prodloužením jedné strany.", + "Je roven součtu dvou vnitřních úhlů, které s ním nesousedí.", + "Vnitřní úhly 40° a 60° dávají vnější úhel 100° u zbývajícího vrcholu.", + "Součet vnějšího úhlu a přilehlého vnitřního je 180°.", + ]), + S("3 / 3 • Mnohoúhelník", "Pravidelný šestiúhelník", + "Tip: součet vnitřních úhlů n-úhelníku je (n − 2) · 180°.", + [ + "Trojúhelník má součet 180°, čtyřúhelník 360°.", + "Obecný vzorec je (n − 2) · 180°.", + "Pravidelný mnohoúhelník má všechny strany i úhly shodné.", + "Pravidelný šestiúhelník má vnitřní úhel 720° / 6 = 120°.", + ]), + ], + [ + N("Vnější úhel trojúhelníku je součet vnitřních úhlů 40° a 60°. Jak je velký ve stupních?", + 40 + 60, + "Vnější úhel se rovná součtu dvou nesousedních vnitřních úhlů: 100°."), + N("Rovnoramenný trojúhelník má úhly při základně 70°. Jak velký je úhel proti základně?", + 180 - 70 - 70, + "180° − 140° = 40°."), + N("Rovnostranný trojúhelník má stranu 6. Jaký je jeho obvod?", + 3 * 6, + "Tři shodné strany: 3 · 6 = 18."), + N("Jak velký je jeden vnitřní úhel pravidelného šestiúhelníku ve stupních?", + (6 - 2) * 180 / 6, + "Součet je (6 − 2) · 180° = 720° a jeden úhel je 120°."), + ], + ), + lesson( + "Obvody a obsahy", "Perimeter and area", + "Kružnice, lichoběžník a Pythagorova věta.", + "The circle, the trapezium and Pythagoras.", + [ + S("1 / 3 • Kružnice", "Obvod a obsah", + "Tip: u výsledku s π nechte π ve výrazu a pište ho jako pi.", + [ + "Kružnice s poloměrem r má obvod 2πr.", + "Obsah kruhu je πr².", + "Pro r = 7 je obvod 14π.", + "Pro r = 3 je obsah 9π.", + ]), + S("2 / 3 • Lichoběžník", "Dvě základny a výška", + "Tip: obsah je průměr základen krát výška.", + [ + "Lichoběžník má právě jednu dvojici rovnoběžných stran, základny a a c.", + "Obsah je (a + c) · v / 2.", + "Základny 4 a 6 a výška 3 dávají obsah 15.", + "Výška je kolmá vzdálenost základen.", + ]), + S("3 / 3 • Pythagoras", "Pravoúhlý trojúhelník", + "Tip: přepona je nejdelší strana a leží proti pravému úhlu.", + [ + "V pravoúhlém trojúhelníku je a² + b² = c².", + "c je přepona, a a b jsou odvěsny.", + "Trojúhelník 5, 12, 13 je pravoúhlý, protože 25 + 144 = 169.", + "Přepona je 13.", + ]), + ], + [ + PI("Jaký je obvod kružnice s poloměrem 7? Nechte výsledek v násobcích π.", + 2 * 7, "Obvod je 2πr = 14π."), + PI("Jaký je obsah kruhu s poloměrem 3? Nechte výsledek v násobcích π.", + 3 ** 2, "Obsah je πr² = 9π."), + N("Lichoběžník má základny 4 a 6 a výšku 3. Jaký je jeho obsah?", + Fraction(4 + 6, 2) * 3, + "(4 + 6) · 3 / 2 = 15."), + N("Pravoúhlý trojúhelník má odvěsny 5 a 12. Jak dlouhá je přepona?", + 13, "5² + 12² = 25 + 144 = 169 = 13²."), + ], + ), + lesson( + "Konstrukce trojúhelníku", "Constructing a triangle", + "Vzdálenost bodů a trojúhelník daný třemi stranami.", + "The distance between points and a triangle given by three sides.", + [ + S("1 / 3 • Body", "Vzdálenost na mřížce", + "Tip: u svislé nebo vodorovné úsečky stačí rozdíl souřadnic.", + [ + "Bod v rovině má souřadnice [x; y].", + "Vzdálenost [x₁; y₁] a [x₂; y₂] je √((x₂ − x₁)² + (y₂ − y₁)²).", + "A = [1; 1] a B = [5; 1] mají vzdálenost 4.", + "Osa y na naší mřížce míří nahoru.", + ]), + S("2 / 3 • SSS", "Trojúhelník ze tří stran", + "Tip: třetí vrchol je průsečík dvou kružnic.", + [ + "Konstrukce SSS použije tři strany.", + "Z bodu A se rýsuje kružnice o poloměru |AC|, z bodu B o poloměru |BC|.", + "Kružnice se protnou ve dvou bodech, pokud trojúhelník existuje.", + "Trojúhelníková nerovnost: součet dvou stran je větší než třetí.", + ]), + S("3 / 3 • Příklad", "Strany 4, 3 a 5", + "Tip: 3² + 4² = 5², pravý úhel je u bodu A.", + [ + "A = [1; 1], B = [5; 1], takže |AB| = 4.", + "Hledáme C tak, aby |AC| = 3 a |BC| = 5.", + "Na mřížce od 0 do 10 a od 0 do 8 leží jen C = [1; 4].", + "Druhý průsečík [1; −2] je pod mřížkou.", + ]), + ], + [ + N("Jaká je vzdálenost bodů A = [1; 1] a B = [5; 1]?", + 4, "Body mají stejné y, vzdálenost je 5 − 1 = 4."), + N("Jaký je obsah pravoúhlého trojúhelníku s odvěsnami 3 a 4?", + Fraction(3 * 4, 2), + "Obsah je (3 · 4) / 2 = 6."), + N("Jaký je obvod trojúhelníku se stranami 3, 4 a 5?", + 3 + 4 + 5, "3 + 4 + 5 = 12."), + DRAW("A = [1; 1] a B = [5; 1] jsou dané. Zakreslete C, pro které je |AC| = 3 a |BC| = 5.", + "points:1,4;fix:1,1;fix:5,1", + "Kružnice se na mřížce protnou v [1; 4]. Bod [1; −2] už na ní není.", + "1,4"), + ], + ), + ] + + +def year2(): + return [ + lesson( + "Lineární funkce", "Linear functions", + "Předpis, průsečík s osou y, směrnice a nulový bod.", + "The formula, the y-intercept, the slope and a root.", + [ + S("1 / 3 • Předpis", "y = ax + b", + "Tip: a je směrnice, b je úsek na ose y.", + [ + "Lineární funkce má předpis f(x) = ax + b.", + "Grafem je přímka.", + "b je hodnota f(0), tedy průsečík s osou y.", + "Pro f(x) = 2x − 3 je průsečík s osou y číslo −3.", + ]), + S("2 / 3 • Směrnice", "O kolik y vzroste", + "Tip: směrnice je podíl změny y a změny x.", + [ + "Mezi body [x₁; y₁] a [x₂; y₂] je a = (y₂ − y₁)/(x₂ − x₁).", + "Body [1; 2] a [3; 8] dávají směrnici (8 − 2)/(3 − 1) = 3.", + "Kladná směrnice znamená růst, záporná pokles.", + "Nulová směrnice je vodorovná přímka.", + ]), + S("3 / 3 • Hodnota a kořen", "Dosazení a rovnice", + "Tip: nulový bod je x, pro které je f(x) = 0.", + [ + "f(4) u funkce 2x − 3 je 8 − 3 = 5.", + "Rovnice 2x − 6 = 0 má řešení x = 3.", + "To je průsečík přímky y = 2x − 6 s osou x.", + "Lineární funkce s a ≠ 0 má právě jeden nulový bod.", + ]), + ], + [ + N("Funkce f(x) = 2x − 3. Kolik je f(4)?", 2 * 4 - 3, + "2 · 4 − 3 = 5."), + N("Jaký je průsečík funkce f(x) = 2x − 3 s osou y?", -3, + "f(0) = −3."), + N("Jaká je směrnice přímky body [1; 2] a [3; 8]?", + Fraction(8 - 2, 3 - 1), + "(8 − 2)/(3 − 1) = 3."), + N("Vyřešte 2x − 6 = 0. Napište x.", Fraction(6, 2), + "2x = 6, takže x = 3."), + ], + ), + lesson( + "Soustavy rovnic", "Systems of equations", + "Dvě lineární rovnice o dvou neznámých.", + "Two linear equations in two unknowns.", + [ + S("1 / 3 • Dvojice", "Jedno x a jedno y", + "Tip: výsledek pište x;y, nejdřív x.", + [ + "Soustava dvou lineárních rovnic hledá dvojici [x; y], která vyhovuje oběma.", + "Řešením je průsečík dvou přímek.", + "Sčítací metoda sečte rovnice tak, aby jedna neznámá zmizela.", + "Dosazovací metoda vyjádří jednu neznámou a dosadí ji do druhé rovnice.", + ]), + S("2 / 3 • Sčítání", "Opačné koeficienty", + "Tip: x + y = 10 a x − y = 2 se sčítají na 2x = 12.", + [ + "Součet dá 2x = 12, tedy x = 6.", + "Z x + y = 10 je y = 4.", + "Zkouška ve druhé rovnici: 6 − 4 = 2.", + "Odečtení rovnic se použije, když má neznámá v obou stejný koeficient.", + ]), + S("3 / 3 • Odečtení", "Stejný koeficient", + "Tip: rovnice lze před odečtením násobit.", + [ + "2x + y = 8 a x + y = 5 se odečtou na x = 3.", + "Pak y = 2.", + "3x + y = 10 a x + y = 6 dají 2x = 4, tedy x = 2 a y = 4.", + "x + y = 7 a x − y = 1 dají x = 4 a y = 3.", + ]), + ], + [ + PAIR("Vyřešte soustavu x + y = 10 a x − y = 2. Napište x a y oddělené středníkem.", + 6, 4, "Sečtení dá 2x = 12, x = 6 a y = 4."), + PAIR("Vyřešte soustavu 2x + y = 8 a x + y = 5. Napište x a y oddělené středníkem.", + 3, 2, "Odečtení dá x = 3 a y = 2."), + PAIR("Vyřešte soustavu x + y = 7 a x − y = 1. Napište x a y oddělené středníkem.", + 4, 3, "Sečtení dá 2x = 8, x = 4 a y = 3."), + PAIR("Vyřešte soustavu 3x + y = 10 a x + y = 6. Napište x a y oddělené středníkem.", + 2, 4, "Odečtení dá 2x = 4, x = 2 a y = 4."), + ], + ), + lesson( + "Kvadratické rovnice", "Quadratic equations", + "Rozklad, diskriminant a dvojnásobný kořen.", + "Factoring, the discriminant and a double root.", + [ + S("1 / 3 • Tvar", "ax² + bx + c = 0", + "Tip: kořeny pište oddělené středníkem, pořadí nehraje roli.", + [ + "Kvadratická rovnice má a ≠ 0.", + "Kořeny rovnice x² − 5x + 6 = 0 jsou 2 a 3, protože (x − 2)(x − 3) = 0.", + "x² − 4 = 0 má kořeny −2 a 2.", + "Součin je nula, právě když je nula aspoň jeden činitel.", + ]), + S("2 / 3 • Diskriminant", "D = b² − 4ac", + "Tip: D > 0 znamená dva různé reálné kořeny.", + [ + "Pro ax² + bx + c = 0 je D = b² − 4ac.", + "U x² − 2x − 3 je a = 1, b = −2, c = −3 a D = 4 + 12 = 16.", + "D = 0 znamená jeden dvojnásobný kořen.", + "D < 0 znamená, že v reálných číslech řešení není.", + ]), + S("3 / 3 • Vzorec", "Kořen je (−b ± √D) / (2a)", + "Tip: dvojnásobný kořen x² + 6x + 9 = 0 je −3.", + [ + "x = (−b ± √D) / (2a).", + "x² + 6x + 9 = (x + 3)², takže kořen −3 je dvojnásobný.", + "U dvojnásobného kořene stačí napsat číslo jednou.", + "Zkouška dosadí kořen zpět do rovnice.", + ]), + ], + [ + SET("Najděte kořeny rovnice x² − 5x + 6 = 0. Oddělte je středníkem.", + [2, 3], "(x − 2)(x − 3) = 0."), + SET("Najděte kořeny rovnice x² − 4 = 0. Oddělte je středníkem.", + [-2, 2], "x² = 4, takže x = −2 nebo x = 2."), + N("Jaký je diskriminant rovnice x² − 2x − 3 = 0?", + (-2) ** 2 - 4 * 1 * (-3), + "D = (−2)² − 4 · 1 · (−3) = 4 + 12 = 16."), + N("Jaký je dvojnásobný kořen rovnice x² + 6x + 9 = 0?", + -3, "(x + 3)² = 0, kořen je −3."), + ], + ), + lesson( + "Lomené výrazy", "Rational expressions", + "Krácení, definiční obor a násobení zlomků s proměnnou.", + "Cancelling, the domain and multiplying algebraic fractions.", + [ + S("1 / 3 • Obor", "Jmenovatel nesmí být nula", + "Tip: podmínku pište dřív, než krátíte.", + [ + "Lomený výraz je podíl dvou mnohočlenů.", + "Není definován tam, kde je jmenovatel nula.", + "(x + 2)/(x − 2) nemá smysl pro x = 2.", + "Po krácení se vyloučené hodnoty do oboru nevracejí.", + ]), + S("2 / 3 • Krácení", "Rozdíl čtverců", + "Tip: x² − 1 = (x − 1)(x + 1).", + [ + "(x² − 1)/(x − 1) = x + 1 pro x ≠ 1.", + "Při x = 4 je hodnota 5.", + "(x² − 4)/(x + 2) = x − 2 pro x ≠ −2.", + "Při x = 5 je hodnota 3.", + ]), + S("3 / 3 • Násobení", "Čitatel s čitatelem", + "Tip: x ve jmenovateli se zkrátí jen pro x ≠ 0.", + [ + "Zlomky se násobí stejně jako číselné.", + "(2x/4) · (6/x) = 12x/(4x) = 3 pro x ≠ 0.", + "6x/(3x) = 2 pro x ≠ 0.", + "Než se zkrátí písmeno, ověří se, že není nula.", + ]), + ], + [ + N("Do výrazu (x² − 1)/(x − 1) dosažte x = 4.", + Fraction(4 ** 2 - 1, 4 - 1), + "Pro x ≠ 1 je výraz x + 1, tedy 5."), + N("Pro které x nemá výraz (x + 2)/(x − 2) smysl?", + 2, "Jmenovatel x − 2 je nula právě při x = 2."), + N("Upravte (2x/4) · (6/x) pro x ≠ 0.", + 3, "12x/(4x) = 3."), + N("Do výrazu (x² − 4)/(x + 2) dosažte x = 5.", + Fraction(25 - 4, 5 + 2), + "Pro x ≠ −2 je výraz x − 2, tedy 3."), + ], + ), + lesson( + "Mocniny s racionálním exponentem", "Rational exponents", + "Odmocniny, součin mocnin a záporný exponent.", + "Roots, a product of powers and a negative exponent.", + [ + S("1 / 3 • Odmocnina jako exponent", "a^(1/n)", + "Tip: a^(1/2) je druhá odmocnina z a.", + [ + "Pro a > 0 je a^(1/n) n-tá odmocnina z a.", + "16^(1/2) = 4.", + "a^(m/n) je n-tá odmocnina z aᵐ, nebo m-tá mocnina odmocniny.", + "8^(2/3) = (8^(1/3))² = 2² = 4.", + ]), + S("2 / 3 • Stejný základ", "Exponenty se sčítají", + "Tip: 3² · 3³ = 3⁵.", + [ + "aᵐ · aⁿ = aᵐ⁺ⁿ.", + "3² · 3³ = 3⁵ = 243.", + "(aᵐ)ⁿ = aᵐ·ⁿ.", + "Základ musí být při sčítání exponentů stejný.", + ]), + S("3 / 3 • Záporný exponent", "Převrácená hodnota", + "Tip: a⁻ⁿ = 1/aⁿ pro a ≠ 0.", + [ + "10⁻² = 1/100.", + "10⁻¹ = 0,1 a 10⁰ = 1.", + "Záporný exponent nepřevrací znaménko základu, ale celou mocninu.", + "(−2)⁻² = 1/4, protože (−2)² = 4.", + ]), + ], + [ + N("Spočítejte 16^(1/2).", 4, "Druhá odmocnina z 16 je 4."), + N("Spočítejte 8^(2/3).", 4, "Třetí odmocnina z 8 je 2 a 2² = 4."), + N("Spočítejte 3² · 3³.", 3 ** 5, "3⁵ = 243."), + N("Spočítejte 10⁻².", Fraction(1, 100), "10⁻² = 1/10² = 1/100."), + ], + ), + lesson( + "Goniometrie pravoúhlého trojúhelníku", "Right-triangle trigonometry", + "Sinus, kosinus a tangens ostrého úhlu.", + "Sine, cosine and tangent of an acute angle.", + [ + S("1 / 3 • Poměry", "Odvěsny a přepona", + "Tip: sinus je protilehlá odvěsna ku přeponě.", + [ + "V pravoúhlém trojúhelníku je sin α = protilehlá / přepona.", + "cos α = přilehlá / přepona.", + "tan α = protilehlá / přilehlá.", + "Přepona je strana proti pravému úhlu.", + ]), + S("2 / 3 • Tabulkové úhly", "30°, 45° a 60°", + "Tip: sinus 30° je 1/2.", + [ + "sin 30° = 1/2 a cos 60° = 1/2.", + "sin 60° = cos 30° = √3/2.", + "sin 45° = cos 45° = √2/2.", + "tan 45° = 1.", + ]), + S("3 / 3 • Dopočet strany", "Přepona a úhel", + "Tip: proti úhlu 30° je polovina přepony.", + [ + "Známe-li přeponu a úhel, protilehlá odvěsna je přepona · sin α.", + "Proti 30° při přeponě 10 je odvěsna 5.", + "Přilehlá odvěsna by byla 10 · cos 30°.", + "Tangens se hodí, když jsou známé dvě odvěsny a ne přepona.", + ]), + ], + [ + N("Kolik je sin 30°?", Fraction(1, 2), + "Sinus 30° je 1/2."), + N("Kolik je cos 60°?", Fraction(1, 2), + "Kosinus 60° je 1/2."), + N("Kolik je tan 45°?", 1, + "Protilehlá a přilehlá odvěsna jsou u 45° stejně dlouhé."), + N("Přepona pravoúhlého trojúhelníku je 10 a jeden úhel je 30°. Jak dlouhá je odvěsna proti tomuto úhlu?", + 10 * Fraction(1, 2), + "10 · sin 30° = 10 · 1/2 = 5."), + ], + ), + lesson( + "Pythagorova a Eukleidovy věty", "Pythagoras and Euclid", + "Přepona, obsah čtverce nad ní a úseky přepony.", + "The hypotenuse, the square on it, and the projections on it.", + [ + S("1 / 3 • Pythagoras", "a² + b² = c²", + "Tip: 8² + 15² = 64 + 225 = 289 = 17².", + [ + "V pravoúhlém trojúhelníku se součet čtverců nad odvěsnami rovná čtverci nad přeponou.", + "Trojúhelník 8, 15, 17 je pravoúhlý a přepona je 17.", + "Čtverec nad přeponou trojúhelníku 3, 4, 5 má obsah 25.", + "Věta platí jen v pravoúhlém trojúhelníku.", + ]), + S("2 / 3 • Eukleides", "Úsek přepony", + "Tip: a² = c · a_c, kde a_c je úsek přepony přilehlý k odvěsně a.", + [ + "Odvěsna je geometrický průměr přepony a svého úseku.", + "a² = c · a_c a b² = c · b_c.", + "Úseky a_c a b_c dají dohromady přeponu c.", + "U odvěsen 6 a 8 je přepona 10 a úsek u odvěsny 6 je 36/10 = 18/5.", + ]), + S("3 / 3 • Výška", "v² = a_c · b_c", + "Tip: také platí a · b = c · v.", + [ + "Výška k přeponě je geometrický průměr obou úseků.", + "Z a · b = c · v plyne v = ab/c.", + "Pro odvěsny 6 a 8 a přeponu 10 je v = 48/10 = 24/5.", + "Obsah trojúhelníku je pak (c · v) / 2, stejně jako (a · b) / 2.", + ]), + ], + [ + N("Pravoúhlý trojúhelník má odvěsny 8 a 15. Jak dlouhá je přepona?", + 17, "64 + 225 = 289 = 17²."), + N("Jaký obsah má čtverec sestrojený nad přeponou pravoúhlého trojúhelníku se stranami 3, 4 a 5?", + 25, "Přepona je 5 a 5² = 25."), + N("Odvěsny jsou 6 a 8, přepona je 10. Jak dlouhý je úsek přepony přilehlý k odvěsně 6?", + Fraction(36, 10), + "a² = c · a_c, takže a_c = 36/10 = 18/5."), + N("Odvěsny jsou 6 a 8, přepona je 10. Jak dlouhá je výška k přeponě?", + Fraction(6 * 8, 10), + "v = ab/c = 48/10 = 24/5."), + ], + ), + lesson( + "Kružnice a Thaletova věta", "Circles and Thales", + "Obvodový a středový úhel, délka oblouku a pravý úhel nad průměrem.", + "An inscribed and a central angle, arc length, and the right angle in a semicircle.", + [ + S("1 / 3 • Úhly", "Obvodový je polovina středového", + "Tip: oba úhly musí být nad stejným obloukem.", + [ + "Středový úhel má vrchol ve středu kružnice.", + "Obvodový úhel má vrchol na kružnici.", + "Nad stejným obloukem je obvodový úhel polovinou středového.", + "Středový úhel 80° patří k obvodovému úhlu 40°.", + ]), + S("2 / 3 • Oblouk", "Část obvodu", + "Tip: délka oblouku je (α/360°) · 2πr.", + [ + "Celý obvod je 2πr.", + "Oblouk 60° je šestina obvodu.", + "Při r = 6 je jeho délka 2π.", + "Výsledek nechte v násobcích π.", + ]), + S("3 / 3 • Thales", "Pravý úhel nad průměrem", + "Tip: vrchol pravého úhlu leží na kružnici s průměrem AB.", + [ + "Thaletova věta: úhel nad průměrem je pravý.", + "Je-li AB průměr, je úhel ACB pravý pro každé C na kružnici různé od A a B.", + "Na mřížce má průměr od [2; 4] do [8; 4] střed [5; 4] a poloměr 3.", + "Vyhovují body [5; 7] a [5; 1].", + ]), + ], + [ + N("Středový úhel je 80°. Jak velký je obvodový úhel nad stejným obloukem ve stupních?", + 40, "Obvodový úhel je polovina středového."), + PI("Kružnice má poloměr 6. Jak dlouhý je oblouk 60°? Nechte π ve výsledku.", + 2, "(60/360) · 2π · 6 = 2π."), + N("Obvodový úhel je 35°. Jak velký je středový úhel nad stejným obloukem ve stupních?", + 70, "Středový úhel je dvojnásobek obvodového."), + DRAW("AB je průměr od [2; 4] do [8; 4]. Zakreslete bod C na kružnici, ve kterém je úhel ACB pravý.", + "right:2,4;8,4", + "Thaletova kružnice má střed [5; 4] a poloměr 3. Na mřížce vyhovuje [5; 7] nebo [5; 1].", + "5,7"), + ], + ), + ] + + +def year3(): + return [ + lesson( + "Kvadratická funkce", "The quadratic function", + "Vrchol, kořeny a hodnota paraboly.", + "The vertex, the roots and a value of a parabola.", + [ + S("1 / 3 • Parabola", "f(x) = ax² + bx + c", + "Tip: pro a > 0 má parabola minimum, pro a < 0 maximum.", + [ + "Grafem kvadratické funkce je parabola.", + "Průsečík s osou y je bod [0; c].", + "U f(x) = x² − 4x + 3 je f(0) = 3.", + "Osa paraboly je svislá.", + ]), + S("2 / 3 • Vrchol", "x = −b/(2a)", + "Tip: souřadnice vrcholu pište x;y.", + [ + "x-ová souřadnice vrcholu je −b/(2a).", + "U x² − 4x + 3 je to −(−4)/2 = 2.", + "f(2) = 4 − 8 + 3 = −1.", + "Vrchol je tedy [2; −1].", + ]), + S("3 / 3 • Kořeny", "Průsečíky s osou x", + "Tip: kořeny jsou řešení f(x) = 0.", + [ + "x² − 4x + 3 = (x − 1)(x − 3).", + "Kořeny jsou 1 a 3.", + "Leží symetricky kolem osy paraboly.", + "f(4) = 16 − 16 + 3 = 3, stejně jako f(0).", + ]), + ], + [ + PAIR("Najděte vrchol funkce f(x) = x² − 4x + 3. Napište x a y oddělené středníkem.", + 2, -1, "x = 4/2 = 2 a f(2) = −1."), + SET("Najděte kořeny funkce f(x) = x² − 4x + 3. Oddělte je středníkem.", + [1, 3], "(x − 1)(x − 3) = 0."), + N("Kolik je f(0) u funkce f(x) = x² − 4x + 3?", 3, + "f(0) = 3, to je absolutní člen."), + N("Kolik je f(4) u funkce f(x) = x² − 4x + 3?", 16 - 16 + 3, + "16 − 16 + 3 = 3."), + ], + ), + lesson( + "Exponenciální funkce a logaritmus", "Exponentials and logarithms", + "Mocnina se základem a logaritmus jako její opak.", + "A power and the logarithm as its inverse.", + [ + S("1 / 3 • Definice", "Logaritmus je exponent", + "Tip: log_b(x) = y právě tehdy, když bʸ = x.", + [ + "Základ logaritmu je kladný a různý od 1.", + "Argument logaritmu je kladný.", + "log₁₀(1000) = 3, protože 10³ = 1000.", + "log₂(32) = 5, protože 2⁵ = 32.", + ]), + S("2 / 3 • Rovnice", "Stejný základ", + "Tip: 2ˣ = 16 se přepíše jako 2ˣ = 2⁴.", + [ + "Exponentialní rovnice se stejným základem porovná exponenty.", + "2ˣ = 16 má řešení x = 4.", + "log₃(81) = 4, protože 3⁴ = 81.", + "log_b(b) = 1 a log_b(1) = 0.", + ]), + S("3 / 3 • Pravidla", "Součin a podíl", + "Tip: logaritmus součinu je součet logaritmů.", + [ + "log_b(xy) = log_b(x) + log_b(y).", + "log_b(x/y) = log_b(x) − log_b(y).", + "log_b(xⁿ) = n · log_b(x).", + "Těmito pravidly se argument zjednodušuje, ne základ.", + ]), + ], + [ + N("Spočítejte logaritmus o základu 10 z 1000.", 3, + "10³ = 1000, takže logaritmus je 3."), + N("Spočítejte logaritmus o základu 2 z 32.", 5, + "2⁵ = 32."), + N("Vyřešte 2ˣ = 16. Napište x.", 4, + "16 = 2⁴, takže x = 4."), + N("Spočítejte logaritmus o základu 3 z 81.", 4, + "3⁴ = 81."), + ], + ), + lesson( + "Goniometrické funkce", "Trigonometric functions", + "Sinus a kosinus na jednotkové kružnici.", + "Sine and cosine on the unit circle.", + [ + S("1 / 3 • Jednotková kružnice", "Úhel od kladné poloosy x", + "Tip: sinus je y-ová souřadnice, kosinus x-ová.", + [ + "Bod na jednotkové kružnici pod úhlem α má souřadnice [cos α; sin α].", + "cos 0° = 1 a sin 0° = 0.", + "sin 90° = 1 a cos 90° = 0.", + "cos 180° = −1 a sin 180° = 0.", + ]), + S("2 / 3 • 45°", "Polovina pravého úhlu", + "Tip: sin 45° = cos 45° = √2/2.", + [ + "Úhel 45° půlí čtvrtkružnici.", + "Souřadnice bodu splňují x = y a x² + y² = 1.", + "Kladné řešení je √2/2.", + "Odpověď lze psát jako sqrt(2)/2 nebo 1/sqrt(2).", + ]), + S("3 / 3 • Znaménka", "Čtyři kvadranty", + "Tip: v prvním kvadrantu jsou sinus i kosinus kladné.", + [ + "Ve druhém kvadrantu je sinus kladný a kosinus záporný.", + "Ve třetím jsou oba záporné.", + "Ve čtvrtém je kosinus kladný a sinus záporný.", + "Tangens je sinus dělený kosinem a není definován při cos α = 0.", + ]), + ], + [ + N("Kolik je sin 90°?", 1, "Bod [0; 1] má y-ovou souřadnici 1."), + N("Kolik je cos 180°?", -1, "Bod [−1; 0] má x-ovou souřadnici −1."), + SQRT("Kolik je sin 45°? Můžete psát sqrt(2)/2.", + "num:sqrt(2)/2", + "sin 45° = √2/2. Stejnou hodnotu má 1/√2."), + N("Kolik je cos 0°?", 1, "Úhel 0° je bod [1; 0]."), + ], + ), + lesson( + "Sinová a kosinová věta", "The sine and cosine rules", + "Obecný trojúhelník, poloměr kružnice a obsah.", + "A general triangle, the circumradius and the area.", + [ + S("1 / 3 • Sinová věta", "a/sin α = 2R", + "Tip: poměr strany a sinu protilehlého úhlu je pro všechny strany stejný.", + [ + "a/sin α = b/sin β = c/sin γ = 2R.", + "R je poloměr kružnice opsané.", + "Když a = 10 a α = 30°, je 10 / (1/2) = 20 = 2R.", + "Poloměr R je 10.", + ]), + S("2 / 3 • Kosinová věta", "c² = a² + b² − 2ab cos γ", + "Tip: pro pravý úhel je cos 90° = 0 a věta přejde v Pythagora.", + [ + "Kosinová věta dopočítá stranu proti známému úhlu.", + "Jsou-li dvě strany 2 a sevřený úhel 60°, je třetí strana také 2.", + "a² = 4 + 4 − 2 · 2 · 2 · cos 60° = 8 − 8 · 1/2 = 4.", + "Rovnostranný trojúhelník má všechny úhly 60°.", + ]), + S("3 / 3 • Obsah", "Dvě strany a sevřený úhel", + "Tip: S = (1/2) · b · c · sin α.", + [ + "Obsah je polovina součinu dvou stran a sinu sevřeného úhlu.", + "(1/2) · 4 · 6 · sin 30° = 12 · 1/2 = 6.", + "Sinová věta také dopočítá další stranu: b = a · sin β / sin α.", + "Pro a = 8, α = 30° a β = 45° je b = 8√2.", + ]), + ], + [ + N("Strana a = 10 leží proti úhlu 30°. Jaký je poloměr kružnice opsané?", + 10, "a/sin 30° = 20 = 2R, takže R = 10."), + N("Dvě strany trojúhelníku mají délku 2 a svírají úhel 60°. Jak dlouhá je třetí strana?", + 2, "a² = 4 + 4 − 8 · 1/2 = 4, takže a = 2."), + N("Dvě strany mají délky 4 a 6 a svírají úhel 30°. Jaký je obsah trojúhelníku?", + Fraction(1, 2) * 4 * 6 * Fraction(1, 2), + "(1/2) · 4 · 6 · 1/2 = 6."), + SQRT("Strana a = 8 je proti 30° a úhel β je 45°. Jak dlouhá je strana b? Sin 45° je √2/2.", + "num:8*sqrt(2)", + "b = 8 · sin 45° / sin 30° = 8 · (√2/2) / (1/2) = 8√2."), + ], + ), + lesson( + "Posloupnosti a řady", "Sequences and series", + "Aritmetická a geometrická posloupnost a nekonečná řada.", + "An arithmetic and a geometric sequence, and an infinite series.", + [ + S("1 / 3 • Aritmetická", "Stálý rozdíl", + "Tip: aₙ = a₁ + (n − 1)d.", + [ + "Aritmetická posloupnost přičítá stále stejný rozdíl d.", + "Pro a₁ = 3 a d = 2 je a₅ = 3 + 4 · 2 = 11.", + "Součet prvních n členů je sₙ = n/2 · (2a₁ + (n − 1)d).", + "s₅ = 5/2 · (6 + 8) = 35.", + ]), + S("2 / 3 • Geometrická", "Stálý kvocient", + "Tip: aₙ = a₁ · qⁿ⁻¹.", + [ + "Geometrická posloupnost násobí stále stejným kvocientem q.", + "Pro a₁ = 2 a q = 3 je a₄ = 2 · 3³ = 54.", + "q = 1 znamená, že se členy nemění.", + "q = 0 je po prvním členu samá nula, pokud je první člen definován.", + ]), + S("3 / 3 • Nekonečná řada", "Součet pro |q| < 1", + "Tip: s = a₁ / (1 − q).", + [ + "Nekonečná geometrická řada má součet jen pro |q| < 1.", + "Pro a₁ = 6 a q = 1/2 je s = 6 / (1/2) = 12.", + "Pro |q| ≥ 1 se součet členů neblíží ke konečnému číslu, kromě případu q = 1 a a₁ = 0.", + "Řada 6 + 3 + 3/2 + … se tedy blíží ke 12.", + ]), + ], + [ + N("Aritmetická posloupnost má a₁ = 3 a d = 2. Kolik je a₅?", + 3 + 4 * 2, "a₅ = 3 + 4 · 2 = 11."), + N("Aritmetická posloupnost má a₁ = 3 a d = 2. Kolik je součet prvních pěti členů?", + Fraction(5, 2) * (2 * 3 + 4 * 2), + "s₅ = 5/2 · (6 + 8) = 35."), + N("Geometrická posloupnost má a₁ = 2 a q = 3. Kolik je a₄?", + 2 * (3 ** 3), "a₄ = 2 · 27 = 54."), + N("Nekonečná geometrická řada má a₁ = 6 a q = 1/2. Jaký je její součet?", + 6 / Fraction(1, 2), "s = 6 / (1/2) = 12."), + ], + ), + lesson( + "Kombinatorika", "Combinatorics", + "Permutace, variace a kombinace.", + "Permutations, variations and combinations.", + [ + S("1 / 3 • Permutace", "Pořadí všech prvků", + "Tip: n! = 1 · 2 · … · n a 0! = 1.", + [ + "Permutace je uspořádání všech n různých prvků.", + "Počet je n!.", + "4! = 24 a 3! = 6.", + "Na prvním místě je n možností, na dalším n − 1, a tak dál.", + ]), + S("2 / 3 • Variace", "Pořadí vybraných prvků", + "Tip: variace bez opakování V(n, k) = n! / (n − k)!.", + [ + "Variace vybírá k prvků z n a záleží na pořadí.", + "V(5, 2) = 5 · 4 = 20.", + "Opakování by dovolilo vybrat stejný prvek víckrát.", + "Permutace je variace, která vybírá všech n prvků.", + ]), + S("3 / 3 • Kombinace", "Pořadí nehraje roli", + "Tip: C(n, k) = n! / (k! · (n − k)!).", + [ + "Kombinace vybírá k prvků a dvě vybrané skupiny se stejným obsahem jsou jedna.", + "C(5, 2) = 10.", + "C(n, k) = C(n, n − k).", + "Počet variací je k! krát větší než počet kombinací.", + ]), + ], + [ + N("Kolik je permutací čtyř různých prvků?", 24, + "4! = 24."), + N("Kolik je kombinací 2 prvků z 5?", 10, + "C(5, 2) = (5 · 4) / 2 = 10."), + N("Kolik je variací 2 prvků z 5 bez opakování?", 20, + "V(5, 2) = 5 · 4 = 20."), + N("Kolik je 3!?", 6, "3 · 2 · 1 = 6."), + ], + ), + lesson( + "Pravděpodobnost", "Probability", + "Klasická pravděpodobnost, kostka, mince a karty.", + "Classical probability with a die, a coin and cards.", + [ + S("1 / 3 • Definice", "Příznivé ku všem", + "Tip: všechny výsledky musí být stejně možné.", + [ + "Klasická pravděpodobnost je počet příznivých výsledků dělený počtem všech.", + "Hodnota leží od 0 do 1.", + "Jev jistý má pravděpodobnost 1, jev nemožný 0.", + "Na kostce je sudé číslo ve třech případech ze šesti, tedy 1/2.", + ]), + S("2 / 3 • Nezávislost", "Součin pravděpodobností", + "Tip: dvě mince mají čtyři stejně možné dvojice výsledků.", + [ + "U dvou nezávislých mincí jsou možnosti PP, PO, OP a OO.", + "Dva panny jsou jedna možnost ze čtyř, tedy 1/4.", + "U dvou kostek je 36 stejně možných dvojic.", + "Součet 7 nastane u šesti dvojic: 1+6, 2+5, 3+4, 4+3, 5+2 a 6+1.", + ]), + S("3 / 3 • Karty", "Čtyři esa v balíčku 32 karet", + "Tip: mariášový balíček má 32 karet.", + [ + "Pravděpodobnost esa je 4/32 = 1/8.", + "Tažení jedné karty považuje každou kartu za stejně možnou.", + "Doplňkový jev má pravděpodobnost 1 − p.", + "Výsledek se krátí do základního tvaru.", + ]), + ], + [ + N("Jaká je pravděpodobnost sudého čísla při hodu hrací kostkou?", + Fraction(3, 6), + "Sudá jsou 2, 4 a 6, tedy 3 ze 6, čili 1/2."), + N("Jaká je pravděpodobnost, že na dvou mincích padnou dvě panny?", + Fraction(1, 4), + "Ze čtyř dvojic je příznivá jedna."), + N("V balíčku 32 karet jsou 4 esa. Jaká je pravděpodobnost, že tažená karta je eso?", + Fraction(4, 32), + "4/32 = 1/8."), + N("Jaká je pravděpodobnost, že součet na dvou hracích kostkách je 7?", + Fraction(6, 36), + "Příznivých dvojic je 6 z 36, tedy 1/6."), + ], + ), + lesson( + "Stereometrie", "Solid geometry", + "Krychle, válec, koule a jehlan.", + "The cube, the cylinder, the sphere and a pyramid.", + [ + S("1 / 3 • Krychle", "Objem a povrch", + "Tip: krychle o hraně a má objem a³ a povrch 6a².", + [ + "Objem je počet jednotkových krychlí, které se do tělesa vejdou.", + "Krychle o hraně 3 má objem 27.", + "Povrch je součet obsahů všech stěn: 6 · 9 = 54.", + "Jednotky objemu jsou krychlové, jednotky povrchu čtvereční.", + ]), + S("2 / 3 • Válec a koule", "Vzorce s π", + "Tip: výsledek nechte v násobcích π.", + [ + "Objem válce je πr²v.", + "Pro r = 2 a v = 5 je objem 20π.", + "Objem koule je (4/3)πr³.", + "Pro r = 3 je objem (4/3)π · 27 = 36π.", + ]), + S("3 / 3 • Jehlan", "Třetina hranolu", + "Tip: objem jehlanu je (1/3) · obsah podstavy · výška.", + [ + "Jehlan má stejný objem jako třetina hranolu se stejnou podstavou a výškou.", + "Podstava o obsahu 12 a výška 4 dávají objem 16.", + "Výška je kolmá vzdálenost vrcholu od roviny podstavy.", + "Stejný vztah třetiny platí i pro kužel vůči válci.", + ]), + ], + [ + N("Krychle má hranu 3. Jaký je její objem?", 3 ** 3, + "3³ = 27."), + N("Krychle má hranu 3. Jaký je její povrch?", 6 * (3 ** 2), + "Šest stěn o obsahu 9 dává 54."), + PI("Válec má poloměr 2 a výšku 5. Jaký je jeho objem? Nechte π ve výsledku.", + (2 ** 2) * 5, "πr²v = π · 4 · 5 = 20π."), + PI("Koule má poloměr 3. Jaký je její objem? Nechte π ve výsledku.", + Fraction(4, 3) * (3 ** 3), + "(4/3)π · 27 = 36π."), + ], + ), + ] + + +def year4(): + return [ + lesson( + "Vektory", "Vectors", + "Velikost, součet, skalární součin a koncový bod.", + "Magnitude, a sum, the dot product and an endpoint.", + [ + S("1 / 3 • Souřadnice", "Velikost a součet", + "Tip: velikost vektoru (a; b) je √(a² + b²).", + [ + "Vektor má velikost a směr.", + "|(3; 4)| = 5, protože 9 + 16 = 25.", + "Vektory se sčítají po souřadnicích.", + "(1; 2) + (3; −1) = (4; 1).", + ]), + S("2 / 3 • Skalární součin", "Číslo, ne vektor", + "Tip: (a; b) · (c; d) = ac + bd.", + [ + "Skalární součin dvou vektorů je číslo.", + "(1; 2) · (3; 4) = 3 + 8 = 11.", + "Nulový skalární součin znamená kolmé vektory.", + "Velikost lze psát jako odmocninu ze skalárního součinu vektoru se sebou.", + ]), + S("3 / 3 • Vzdálenost", "Velikost rozdílu", + "Tip: na mřížce osa y míří nahoru.", + [ + "Vzdálenost bodů je velikost vektoru mezi nimi.", + "Od [1; 1] do [4; 5] je vektor (3; 4) a vzdálenost 5.", + "Koncový bod vektoru (3; 2) umístěného do [1; 1] je [4; 3].", + "Pořadí souřadnic je x a potom y.", + ]), + ], + [ + N("Jaká je velikost vektoru (3; 4)?", 5, + "√(9 + 16) = √25 = 5."), + PAIR("Sečtěte vektory (1; 2) a (3; −1). Napište souřadnice oddělené středníkem.", + 4, 1, "(1 + 3; 2 + (−1)) = (4; 1)."), + N("Spočítejte skalární součin vektorů (1; 2) a (3; 4).", + 1 * 3 + 2 * 4, "1 · 3 + 2 · 4 = 11."), + N("Jaká je vzdálenost bodů [1; 1] a [4; 5]?", 5, + "Rozdíl je (3; 4) a jeho velikost je 5."), + DRAW("Z bodu A = [1; 1] naneste vektor (3; 2). Zakreslete koncový bod.", + "points:4,3;fix:1,1", + "Ke každé souřadnici se přičte složka vektoru: [1 + 3; 1 + 2] = [4; 3].", + "4,3"), + ], + ), + lesson( + "Analytická přímka", "The line in the plane", + "Směrnice, úsek, parametrické vyjádření a vzdálenost od přímky.", + "Slope, intercept, a parametric equation and the distance from a line.", + [ + S("1 / 3 • Směrnice", "y = ax + b", + "Tip: a = (y₂ − y₁)/(x₂ − x₁).", + [ + "Body [1; 2] a [4; 8] určují směrnici (8 − 2)/(4 − 1) = 2.", + "Přímka procházející bodem [0; 1] se směrnicí 2 má rovnici y = 2x + 1.", + "Úsek na ose y je 1.", + "Svislá přímka směrnici tohoto tvaru nemá.", + ]), + S("2 / 3 • Parametr", "Bod a směrový vektor", + "Tip: X = A + t · u.", + [ + "Parametrické vyjádření je x = x₀ + t · u₁, y = y₀ + t · u₂.", + "Přímka (1; 3) + t(2; 0) má při t = 2 bod [5; 3].", + "Každé t dá právě jeden bod přímky.", + "Směrový vektor (2; 0) je vodorovný.", + ]), + S("3 / 3 • Vzdálenost", "Bod od přímky", + "Tip: pro ax + by + c = 0 je vzdálenost |ax₀ + by₀ + c| / √(a² + b²).", + [ + "Přímka 3x + 4y − 10 = 0 má a = 3, b = 4 a c = −10.", + "Vzdálenost počátku je |−10| / √(9 + 16) = 10/5 = 2.", + "Čitatel je absolutní hodnota, vzdálenost není záporná.", + "Čtvrtý vrchol rovnoběžníku k bodům A, B a C je B + C − A.", + ]), + ], + [ + N("Jaká je směrnice přímky body [1; 2] a [4; 8]?", + Fraction(8 - 2, 4 - 1), + "(8 − 2)/(4 − 1) = 2."), + N("Přímka má rovnici y = 2x + 1. Jaký je její úsek na ose y?", + 1, "Úsek je absolutní člen, tedy 1."), + PAIR("Bod přímky je (1; 3) + t(2; 0). Kam přímka dojde pro t = 2? Napište x a y středníkem.", + 5, 3, "(1; 3) + 2 · (2; 0) = (5; 3)."), + N("Jaká je vzdálenost bodu [0; 0] od přímky 3x + 4y − 10 = 0?", + 2, "|−10| / √(9 + 16) = 10/5 = 2."), + DRAW("A = [1; 1], B = [4; 1] a C = [2; 3]. Zakreslete čtvrtý vrchol rovnoběžníku, ve kterém jsou AB a AC sousední strany.", + "para:1,1;4,1;2,3", + "D = B + C − A = [4 + 2 − 1; 1 + 3 − 1] = [5; 3].", + "5,3"), + ], + ), + lesson( + "Kuželosečky", "Conic sections", + "Kružnice a elipsa v osové poloze.", + "A circle and an ellipse aligned with the axes.", + [ + S("1 / 3 • Kružnice", "Střed a poloměr", + "Tip: (x − m)² + (y − n)² = r² má střed [m; n].", + [ + "Kružnice je množina bodů stejně vzdálených od středu.", + "(x − 1)² + (y + 2)² = 9 má střed [1; −2].", + "y + 2 je totéž co y − (−2).", + "Poloměr je √9 = 3, ne 9.", + ]), + S("2 / 3 • Elipsa", "Dvě poloosy", + "Tip: ve tvaru x²/a² + y²/b² = 1 je a poloosa na ose x.", + [ + "Elipsa v osové poloze se středem v počátku má rovnici x²/a² + y²/b² = 1.", + "U x²/25 + y²/9 = 1 je a = 5 a b = 3.", + "Delší poloosa je hlavní, kratší vedlejší.", + "Vrcholy na ose x jsou [±a; 0], na ose y [0; ±b].", + ]), + S("3 / 3 • Excentricita", "Vzdálenost ohniska od středu", + "Tip: pro a > b je e = √(a² − b²).", + [ + "Lineární výstřednost e splňuje e² = a² − b², když je a hlavní poloosa.", + "√(25 − 9) = √16 = 4.", + "Ohniska elipsy jsou [±e; 0].", + "Součet vzdáleností bodu elipsy od obou ohnisek je 2a.", + ]), + ], + [ + PAIR("Kružnice má rovnici (x − 1)² + (y + 2)² = 9. Napište souřadnice středu jako x;y.", + 1, -2, "Střed je [1; −2]."), + N("Jaký je poloměr kružnice (x − 1)² + (y + 2)² = 9?", + 3, "Pravá strana je r², takže r = 3."), + N("Elipsa má rovnici x²/25 + y²/9 = 1. Jak dlouhá je poloosa a na ose x?", + 5, "a² = 25, takže a = 5."), + N("Elipsa má rovnici x²/25 + y²/9 = 1. Jak dlouhá je poloosa b na ose y?", + 3, "b² = 9, takže b = 3."), + N("Elipsa má a = 5 a b = 3. Jaká je její lineární výstřednost e?", + 4, "e = √(25 − 9) = √16 = 4."), + ], + ), + lesson( + "Komplexní čísla", "Complex numbers", + "Součet, součin, absolutní hodnota a mocnina i.", + "A sum, a product, the modulus and a power of i.", + [ + S("1 / 3 • Tvar", "a + bi", + "Tip: i² = −1. Reálnou a imaginární část pište oddělené středníkem.", + [ + "Komplexní číslo a + bi má reálnou část a a imaginární část b.", + "i je imaginární jednotka a i² = −1.", + "Čísla se sčítají po složkách.", + "(2 + 3i) + (1 − i) = 3 + 2i.", + ]), + S("2 / 3 • Násobení", "Jako dvojčlen", + "Tip: (1 + i)(1 − i) je rozdíl čtverců.", + [ + "(a + bi)(c + di) = ac + adi + bci + bdi².", + "bdi² = −bd, proto se imaginární jednotka ve výsledku nemusí objevit.", + "(1 + i)(1 − i) = 1 − (i)² = 1 − (−1) = 2.", + "Komplexní čísla lze násobit i v goniometrickém tvaru sčítáním argumentů.", + ]), + S("3 / 3 • Absolutní hodnota", "Vzdálenost od nuly", + "Tip: |a + bi| = √(a² + b²).", + [ + "Absolutní hodnota je vzdálenost obrazu čísla od počátku Gaussovy roviny.", + "|3 + 4i| = 5.", + "|z| je vždy reálné nezáporné číslo.", + "i² = −1, i³ = −i a i⁴ = 1.", + ]), + ], + [ + PAIR("Sečtěte 2 + 3i a 1 − i. Napište reálnou a imaginární část středníkem.", + 3, 2, "(2 + 1) + (3 − 1)i = 3 + 2i."), + N("Spočítejte (1 + i)(1 − i).", 2, + "1 − i² = 1 − (−1) = 2."), + N("Jaká je absolutní hodnota čísla 3 + 4i?", 5, + "√(9 + 16) = 5."), + N("Kolik je i²?", -1, "Imaginární jednotka splňuje i² = −1."), + ], + ), + lesson( + "Derivace", "The derivative", + "Derivace mocniny, tečna a derivace sinu.", + "The derivative of a power, a tangent and the derivative of sine.", + [ + S("1 / 3 • Mocnina", "(xⁿ)' = n xⁿ⁻¹", + "Tip: derivace je směrnice tečny.", + [ + "Derivace funkce v bodě je směrnice tečny grafu v tom bodě.", + "(x³)' = 3x². V bodě x = 2 je 3 · 4 = 12.", + "(5x²)' = 10x. V bodě x = 1 je 10.", + "Derivace konstanty je 0 a konstanta se vytýká.", + ]), + S("2 / 3 • Tečna", "Směrnice v bodě", + "Tip: u f(x) = x² je f'(x) = 2x.", + [ + "Tečna ke grafu y = x² v bodě x = 1 má směrnici 2.", + "Rovnice tečny používá bod [1; 1] a tuto směrnici.", + "Kladná derivace znamená, že funkce v bodě roste.", + "Záporná derivace znamená, že funkce klesá.", + ]), + S("3 / 3 • Sinus", "Derivace sinu je kosinus", + "Tip: (sin x)' = cos x a cos 0 = 1.", + [ + "Derivace funkce sin x je cos x.", + "Derivace funkce cos x je −sin x.", + "V bodě x = 0 je derivace sinu rovna cos 0 = 1.", + "Úhel v tomto vzorci je v radiánech, ale nula je nula v obou jednotkách.", + ]), + ], + [ + N("Jaká je derivace funkce x³ v bodě x = 2?", 3 * (2 ** 2), + "(x³)' = 3x² a 3 · 4 = 12."), + N("Jaká je derivace funkce 5x² v bodě x = 1?", 10 * 1, + "(5x²)' = 10x a v jedničce je 10."), + N("Jakou směrnici má tečna ke grafu y = x² v bodě x = 1?", 2, + "Derivace 2x je v bodě 1 rovna 2."), + N("Jaká je derivace funkce sin x v bodě x = 0? Derivace sinu je kosinus.", + 1, "(sin x)' = cos x a cos 0 = 1."), + ], + ), + lesson( + "Průběh funkce", "The shape of a function", + "Stacionární body, lokální extrémy a nulová derivace.", + "Stationary points, local extrema and a zero derivative.", + [ + S("1 / 3 • Stacionární bod", "Derivace je nula", + "Tip: f'(x) = 0 je vodorovná tečna.", + [ + "V lokálním extrému diferencovatelné funkce je derivace nulová.", + "U f(x) = x³ − 3x je f'(x) = 3x² − 3.", + "3(x² − 1) = 0 dává x = −1 a x = 1.", + "To jsou jediní kandidáti na lokální extrém.", + ]), + S("2 / 3 • Maximum a minimum", "Znaménko druhé derivace", + "Tip: f''(x) = 6x. Záporná druhá derivace znamená lokální maximum.", + [ + "V x = −1 je f''(−1) = −6 < 0, takže jde o lokální maximum.", + "f(−1) = −1 + 3 = 2.", + "V x = 1 je f''(1) = 6 > 0, takže jde o lokální minimum.", + "f(1) = 1 − 3 = −2.", + ]), + S("3 / 3 • Jiný příklad", "Lineární derivace", + "Tip: g'(x) = 2x − 4 je nula při x = 2.", + [ + "Kde je derivace nula, má graf vodorovnou tečnu.", + "Rovnice 2x − 4 = 0 má řešení x = 2.", + "Pro x < 2 je derivace 2x − 4 záporná, funkce klesá.", + "Pro x > 2 je derivace kladná, funkce roste.", + ]), + ], + [ + N("Funkce f(x) = x³ − 3x. Ve kterém x má lokální maximum?", + -1, "f'(x) = 3x² − 3 = 0 pro x = ±1 a v −1 je maximum."), + N("Funkce f(x) = x³ − 3x. Jaká je hodnota lokálního maxima?", + (-1) ** 3 - 3 * (-1), + "f(−1) = −1 + 3 = 2."), + N("Funkce f(x) = x³ − 3x. Ve kterém x má lokální minimum?", + 1, "Druhá derivace 6x je v x = 1 kladná."), + N("Derivace funkce je g'(x) = 2x − 4. Pro které x je derivace nulová?", + 2, "2x − 4 = 0 dává x = 2."), + ], + ), + lesson( + "Integrál", "The integral", + "Určitý integrál mocniny jako obsah pod grafem.", + "The definite integral of a power as the area under the graph.", + [ + S("1 / 3 • Primitivní funkce", "Opačný postup k derivaci", + "Tip: primitivní funkce k xⁿ je xⁿ⁺¹/(n + 1) pro n ≠ −1.", + [ + "Neurčitý integrál je množina primitivních funkcí.", + "Liší se o konstantu, protože derivace konstanty je nula.", + "Primitivní funkce k 2x je x².", + "Primitivní funkce k x² je x³/3.", + ]), + S("2 / 3 • Určitý integrál", "Newtonův vzorec", + "Tip: ∫ od a do b z f je F(b) − F(a).", + [ + "Určitý integrál nezáporné funkce je obsah plochy pod grafem.", + "∫ od 0 do 3 z 2x dx = [x²] od 0 do 3 = 9.", + "∫ od 0 do 2 z x² dx = [x³/3] od 0 do 2 = 8/3.", + "Dolní mez 0 u těchto mocnin často vynuluje první člen.", + ]), + S("3 / 3 • Konstanta", "Násobek lze vytknout", + "Tip: integrál z c · f je c krát integrál z f.", + [ + "∫ od 0 do 1 z 3x² dx = [x³] od 0 do 1 = 1.", + "∫ od 0 do 2 z 6x dx = [3x²] od 0 do 2 = 12.", + "Záporná funkce dává záporný integrál, nejde vždy o obsah beze znaménka.", + "Součet funkcí se integruje člen po členu.", + ]), + ], + [ + N("Spočítejte integrál od 0 do 3 z funkce 2x.", + 3 ** 2 - 0, "Primitivní funkce je x² a 9 − 0 = 9."), + N("Spočítejte integrál od 0 do 2 z funkce x².", + Fraction(2 ** 3, 3), + "Primitivní funkce je x³/3 a 8/3 − 0 = 8/3."), + N("Spočítejte integrál od 0 do 1 z funkce 3x².", + 1, "Primitivní funkce je x³ a 1 − 0 = 1."), + N("Spočítejte integrál od 0 do 2 z funkce 6x.", + 3 * (2 ** 2), + "Primitivní funkce je 3x² a 12 − 0 = 12."), + ], + ), + lesson( + "Maturitní mix", "Maturita mix", + "Logaritmus, kombinace, vektor, zlomek a kvadratická rovnice.", + "A logarithm, a combination, a vector, a fraction and a quadratic equation.", + [ + S("1 / 3 • Číslo", "Logaritmus a zlomek", + "Tip: log₂(8) = 3, protože 2³ = 8.", + [ + "Logaritmus vrací exponent.", + "1/2 + 1/3 = 5/6.", + "Společný jmenovatel se hledá i v maturitním příkladu stejně jako na základní škole.", + "Výsledek se krátí.", + ]), + S("2 / 3 • Počet a vektor", "Kombinace a velikost", + "Tip: C(6, 2) = 15 a |(3; 4)| = 5.", + [ + "C(6, 2) = (6 · 5) / 2 = 15.", + "Pořadí vybrané dvojice se nepočítá dvakrát.", + "Velikost vektoru (3; 4) je přepona trojúhelníku 3-4-5.", + "Skalární součin by u kolmých vektorů vyšel nula.", + ]), + S("3 / 3 • Rovnice", "x² − 1 = 0", + "Tip: rozdíl čtverců (x − 1)(x + 1).", + [ + "x² − 1 = 0 má kořeny −1 a 1.", + "Oba se napíšou, oddělené středníkem.", + "Diskriminant je 0 − 4 · 1 · (−1) = 4.", + "Zkouška: 1 − 1 = 0 a 1 − 1 = 0.", + ]), + ], + [ + N("Spočítejte logaritmus o základu 2 z 8.", 3, + "2³ = 8."), + N("Kolik je kombinací 2 prvků ze 6?", 15, + "C(6, 2) = 15."), + N("Jaká je velikost vektoru (3; 4)?", 5, + "√(9 + 16) = 5."), + N("Spočítejte 1/2 + 1/3.", Fraction(1, 2) + Fraction(1, 3), + "3/6 + 2/6 = 5/6."), + SET("Najděte kořeny rovnice x² − 1 = 0. Oddělte je středníkem.", + [-1, 1], "(x − 1)(x + 1) = 0."), + ], + ), + ] + + +YEARS = [ + { + "prefix": "mat0", + "year": 0, + "array": "mat0_lessons", + "add": "add_mat0_pages", + "title_key": "mat0_year", + "title_cs": "Opakování základní školy", + "title_en": "Elementary-school review", + "sub_key": "mat0_sub", + "sub_cs": "Pořadí operací, zlomky, procenta, rovnice a rýsování.", + "sub_en": "Order of operations, fractions, percentages, equations and a construction.", + "lessons": year0(), + }, + { + "prefix": "mat", + "year": 1, + "array": "mat_lessons", + "add": "add_mat_pages", + "title_key": "mat_year1", + "title_cs": "1. ročník", + "title_en": "Year 1", + "sub_key": "mat_sub", + "sub_cs": "Dělitelnost, výrazy, rovnice, planimetrie a konstrukce.", + "sub_en": "Divisibility, expressions, equations, plane figures and a construction.", + "lessons": year1(), + }, + { + "prefix": "mat2", + "year": 2, + "array": "mat2_lessons", + "add": "add_mat2_pages", + "title_key": "mat_year2", + "title_cs": "2. ročník", + "title_en": "Year 2", + "sub_key": "mat2_sub", + "sub_cs": "Funkce, soustavy, kvadratické rovnice, goniometrie a kružnice.", + "sub_en": "Functions, systems, quadratic equations, trigonometry and circles.", + "lessons": year2(), + }, + { + "prefix": "mat3", + "year": 3, + "array": "mat3_lessons", + "add": "add_mat3_pages", + "title_key": "mat_year3", + "title_cs": "3. ročník", + "title_en": "Year 3", + "sub_key": "mat3_sub", + "sub_cs": "Logaritmus, posloupnosti, pravděpodobnost a stereometrie.", + "sub_en": "Logarithms, sequences, probability and solid geometry.", + "lessons": year3(), + }, + { + "prefix": "mat4", + "year": 4, + "array": "mat4_lessons", + "add": "add_mat4_pages", + "title_key": "mat_year4", + "title_cs": "4. ročník", + "title_en": "Year 4", + "sub_key": "mat4_sub", + "sub_cs": "Analytická geometrie, derivace, integrál a maturitní opakování.", + "sub_en": "Coordinate geometry, derivatives, integrals and a maturita review.", + "lessons": year4(), + }, +] + + +def emit_year(info) -> str: + prefix = info["prefix"] + lines = [ + '#include "graduately.h"', + "", + f"NetLesson {info['array']}[MATH_N] = {{", + ] + for i in range(1, 9): + lines.append( + f' {{ .n_slides = 3, .unit_page = "{prefix}unit{i}", .ex_page = "{prefix}ex{i}" }},' + ) + lines.append("};") + lines.append("") + for i, lesson in enumerate(info["lessons"], start=1): + lines.append(f"static GtkWidget *build_{prefix}_unit{i}_page(void) {{") + lines.append(" static const NetSlide slides[] = {") + for kicker, title, tip, body in lesson["slides"]: + lines.append(" {") + lines.append(f" {c_string(kicker)}, {c_string(title)},") + lines.append(f" {c_string(tip)},") + lines.append(" {") + for row in body: + lines.append(f" {c_string(row)},") + lines.append(" NULL,") + lines.append(" },") + lines.append(" },") + lines.append(" };") + lines.append("") + lines.append( + f' return math_unit_page({info["year"]}, &{info["array"]}[{i - 1}], ' + f'"{prefix}_unit{i}", "{prefix}_unit{i}_sub",' + ) + lines.append(" slides, G_N_ELEMENTS(slides));") + lines.append("}") + lines.append("") + lines.append(f"static GtkWidget *build_{prefix}_unit{i}_exercise_page(void) {{") + lines.append(" static const MathItem qs[] = {") + for prob in lesson["problems"]: + lines.append( + f" {{{c_string(prob.prompt)}, {c_string(prob.spec)}, {c_string(prob.hint)}}}," + ) + lines.append(" };") + lines.append("") + lines.append( + f' return math_practice_page({info["year"]}, {i - 1}, "{prefix}unit{i}",' + ) + lines.append( + f' "{prefix}_ex{i}_title", "{prefix}_quiz{i}_head",' + ) + lines.append(" qs, G_N_ELEMENTS(qs));") + lines.append("}") + lines.append("") + lines.append(f"void {info['add']}(GtkStack *stack) {{") + lines.append(" typedef GtkWidget *(*MathBuilder)(void);") + lines.append(" static const struct {") + lines.append(" const char *unit_name;") + lines.append(" const char *ex_name;") + lines.append(" MathBuilder build_unit;") + lines.append(" MathBuilder build_ex;") + lines.append(" } pages[] = {") + for i in range(1, 9): + lines.append( + f' {{"{prefix}unit{i}", "{prefix}ex{i}", ' + f"build_{prefix}_unit{i}_page, build_{prefix}_unit{i}_exercise_page}}," + ) + lines.append(" };") + lines.append("") + lines.append(" for (guint i = 0; i < G_N_ELEMENTS(pages); i++) {") + lines.append(" gtk_stack_add_named(stack, pages[i].build_unit(), pages[i].unit_name);") + lines.append(" gtk_stack_add_named(stack, pages[i].build_ex(), pages[i].ex_name);") + lines.append(" }") + lines.append("}") + lines.append("") + return "\n".join(lines) + + +def emit_i18n() -> str: + rows = [ + ("mat_years_sub", + "Opakování základní školy a čtyři ročníky gymnázia.", + "Elementary review and four years of gymnasium mathematics."), + ("math_quiz_intro", + "Napište výsledek. Zlomky jako 3/4, desetinnou čárku i tečku, π jako pi, odmocninu jako sqrt(2) nebo √2. Více čísel oddělte středníkem.", + "Write the result. Fractions as 3/4, a decimal comma or point, π as pi, a root as sqrt(2) or √2. Separate several numbers with a semicolon."), + ("math_answer_ph", "výsledek", "result"), + ("math_undo", "Zpět", "Undo"), + ("math_clear", "Smazat body", "Clear points"), + ("math_draw_hint", "Klepněte na mřížku. Osa y směřuje nahoru.", + "Tap the grid. The y-axis points up."), + ] + for info in YEARS: + rows.append((info["title_key"], info["title_cs"], info["title_en"])) + rows.append((info["sub_key"], info["sub_cs"], info["sub_en"])) + for i, lesson in enumerate(info["lessons"], start=1): + p = info["prefix"] + rows.append((f"{p}_unit{i}", lesson["title_cs"], lesson["title_en"])) + rows.append((f"{p}_unit{i}_sub", lesson["sub_cs"], lesson["sub_en"])) + rows.append((f"{p}_ex{i}_title", "Příklady", "Practice")) + rows.append((f"{p}_quiz{i}_head", lesson["title_cs"], lesson["title_en"])) + lines = [] + for key, cs, en in rows: + lines.append(f" {{{c_string(key)}, {c_string(cs)}, {c_string(en)}}},") + return "\n".join(lines) + "\n" + + +def cases() -> list[str]: + out = [] + for info in YEARS: + for lesson in info["lessons"]: + for prob in lesson["problems"]: + if prob.draw: + out.append(f"draw {prob.spec}\t{prob.draw}\t1") + else: + kind, _, body = prob.spec.partition(":") + user = body.split("|", 1)[0] if kind == "word" else body + out.append(f"{prob.spec}\t{user}\t1") + out.append("draw square:2,3;5,3\t5,0;2,0\t1") + out.append("draw right:2,4;8,4\t5,1\t1") + return out + + +def main() -> int: + for info in YEARS: + if len(info["lessons"]) != 8: + raise SystemExit(f"{info['prefix']} has {len(info['lessons'])} lessons") + text = emit_year(info) + name = { + "mat0": "mat_year0.c", + "mat": "mat_year1.c", + "mat2": "mat_year2.c", + "mat3": "mat_year3.c", + "mat4": "mat_year4.c", + }[info["prefix"]] + (SRC / name).write_text(text, encoding="utf-8") + print(f"wrote {name}") + (SRC / "math_i18n.inc").write_text(emit_i18n(), encoding="utf-8") + case_path = Path("/tmp/math_cases.txt") + case_path.write_text("\n".join(cases()) + "\n", encoding="utf-8") + binary = Path("/tmp/mathcheck") + if binary.exists(): + result = subprocess.run([str(binary), str(case_path)], check=False) + if result.returncode != 0: + return result.returncode + print("generated answers accepted") + else: + print("checker binary missing, cases written only") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/graduately.h b/src/graduately.h index 314e7d4..27d5ba0 100644 --- a/src/graduately.h +++ b/src/graduately.h @@ -20,6 +20,7 @@ #define HW_SUBJ 2 /* index of "Technické vybavení" */ #define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */ #define ON_SUBJ 4 /* index of "Občanská nauka" */ +#define MATH_SUBJ 6 /* index of "Matematika" */ #define PHY_SUBJ 7 /* index of "Fyzika" */ #define SCI_SUBJ 8 /* index of "Základy Přírodopisných věd" */ #define CSS_FILE "style.css" @@ -67,6 +68,11 @@ extern char *app_progress_fyz; extern char *app_progress_fyz2; extern char *app_progress_fyz3; extern char *app_progress_fyz4; +extern char *app_progress_mat0; +extern char *app_progress_mat; +extern char *app_progress_mat2; +extern char *app_progress_mat3; +extern char *app_progress_mat4; #define PROGRESS_DIR app_progress_dir #define PROGRESS_U1 app_progress_u1 #define PROGRESS_U2 app_progress_u2 @@ -146,6 +152,7 @@ extern char *app_progress_fyz4; #define LIT_N 8 #define SCI_N 8 #define FYZ_N 8 +#define MATH_N 8 #define HW_SLIDES 3 #define HW2_SLIDES 6 #define HW3_SLIDES 2 @@ -345,6 +352,11 @@ typedef struct { int n_options; int correct; } ChoiceQ; +typedef struct { + const char *prompt; + const char *answer; + const char *hint; +} MathItem; typedef struct { const ChoiceQ *qs; int n; @@ -979,6 +991,37 @@ extern NetLesson fyz_lessons[FYZ_N]; extern NetLesson fyz2_lessons[FYZ_N]; extern NetLesson fyz3_lessons[FYZ_N]; extern NetLesson fyz4_lessons[FYZ_N]; +gboolean math_answer_ok(const char *spec, const char *user); +gboolean math_is_draw(const char *spec); +int math_draw_need(const char *spec); +int math_draw_fixed(const char *spec, int *xs, int *ys, int cap); +gboolean math_draw_ok(const char *spec, const int *xs, const int *ys, int n); +void math_load_progress(void); +void math_rail_theme_reset(void); +void math_lessons_apply_lang(void); +void progress_for_math(ProgressSum *out); +GtkWidget *math_unit_page(int year, NetLesson *L, const char *title_key, + const char *sub_key, const NetSlide *slides, + guint n_slides); +GtkWidget *math_practice_page(int year, int lesson_id, const char *back_page, + const char *title_key, const char *heading_key, + const MathItem *items, int n); +GtkWidget *build_matyears_page(void); +GtkWidget *build_mat0map_page(void); +GtkWidget *build_matmap_page(void); +GtkWidget *build_mat2map_page(void); +GtkWidget *build_mat3map_page(void); +GtkWidget *build_mat4map_page(void); +void add_mat0_pages(GtkStack *stack); +void add_mat_pages(GtkStack *stack); +void add_mat2_pages(GtkStack *stack); +void add_mat3_pages(GtkStack *stack); +void add_mat4_pages(GtkStack *stack); +extern NetLesson mat0_lessons[MATH_N]; +extern NetLesson mat_lessons[MATH_N]; +extern NetLesson mat2_lessons[MATH_N]; +extern NetLesson mat3_lessons[MATH_N]; +extern NetLesson mat4_lessons[MATH_N]; void hw_lessons_apply_lang(void); GtkWidget *build_hwyears_page(void); GtkWidget *build_hwmap_page(void); @@ -1068,6 +1111,8 @@ void draw_civics_icon(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data); void draw_science_icon(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data); +void draw_math_icon(GtkDrawingArea *area, cairo_t *cr, + int width, int height, gpointer data); void draw_physics_icon(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data); void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr, diff --git a/src/i18n.c b/src/i18n.c index da44ef8..746b0ad 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -90,12 +90,12 @@ const TrEntry tr_ui[] = { "Cvičení, nápovědy a vokabeltraining. Aplikace si pamatuje, kde jste skončili.", "Exercises, hints and vocabulary drills. The app remembers where you left off."}, {"welcome_feat1_link", "Začít procvičovat", "Start practicing"}, - {"welcome_feat2_title", "Sedm otevřených předmětů", "Seven open subjects"}, + {"welcome_feat2_title", "Osm otevřených předmětů", "Eight open subjects"}, {"welcome_feat2_body", - "Deutsch, sítě, hardware, čeština, občanská nauka, fyzika a přírodní vědy. Ostatní předměty čekají na svůj obsah.", - "German, networks, hardware, Czech, civics, physics and natural sciences. The other subjects are still locked."}, + "Deutsch, sítě, hardware, čeština, občanská nauka, matematika, fyzika a přírodní vědy. Ostatní předměty čekají na svůj obsah.", + "German, networks, hardware, Czech, civics, mathematics, physics and natural sciences. The other subjects are still locked."}, {"welcome_feat2_link", "Jaké předměty jsou otevřené?", "Which subjects are open?"}, - {"welcome_stat_value", "7", "7"}, + {"welcome_stat_value", "8", "8"}, {"welcome_stat_label", "otevřené\npředměty", "open\nsubjects"}, {"continue", "Pokračuj", "Continue"}, {"roadmap_title", "Učební plán", "Learning path"}, @@ -1619,6 +1619,7 @@ const TrEntry tr_ui[] = { {"lit_fuks_plot_title", "Sestavte děj", "Order the plot"}, {"lit_fuks_plot_sub", "Přetáhněte části příběhu do správného pořadí.", "Drag the story parts into the correct order."}, +#include "math_i18n.inc" {NULL, NULL, NULL} }; @@ -2603,6 +2604,7 @@ void on4_lessons_apply_lang(void); void lit_lessons_apply_lang(void); void sci_lessons_apply_lang(void); void fyz_lessons_apply_lang(void); +void math_lessons_apply_lang(void); void net_rail_theme_reset(void); void apply_language(void) { @@ -2624,5 +2626,6 @@ void apply_language(void) { lit_lessons_apply_lang(); sci_lessons_apply_lang(); fyz_lessons_apply_lang(); + math_lessons_apply_lang(); search_apply_lang(); } diff --git a/src/icons.c b/src/icons.c index 7db09e1..befd6c3 100644 --- a/src/icons.c +++ b/src/icons.c @@ -261,6 +261,34 @@ void draw_physics_icon(GtkDrawingArea *area, cairo_t *cr, cairo_stroke(cr); } +/* Axes and a rising curve for Matematika. */ +void draw_math_icon(GtkDrawingArea *area, cairo_t *cr, + int width, int height, gpointer data) { + const double s = MIN(width, height); + const double left = width * 0.22; + const double right = width * 0.82; + const double bottom = height * 0.78; + const double top = height * 0.22; + const Rgb c = mix_rgb(color_from_hex(app_theme.on_accent), + color_from_hex(app_theme.accent2), 0.22); + + (void)area; + (void)data; + cairo_set_source_rgb(cr, c.r, c.g, c.b); + cairo_set_line_width(cr, MAX(1.6, s * 0.07)); + cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); + cairo_move_to(cr, left, top); + cairo_line_to(cr, left, bottom); + cairo_line_to(cr, right, bottom); + cairo_stroke(cr); + + cairo_move_to(cr, left + s * 0.06, bottom - s * 0.08); + cairo_curve_to(cr, left + s * 0.28, bottom - s * 0.02, + width * 0.48, height * 0.62, + right - s * 0.04, top + s * 0.06); + cairo_stroke(cr); +} + /* Open book glyph, painted in the given colour. */ static void book_paint(cairo_t *cr, int width, int height, Rgb c) { const double w = width; diff --git a/src/main.c b/src/main.c index bdc81d9..4c42c62 100644 --- a/src/main.c +++ b/src/main.c @@ -390,6 +390,9 @@ static gboolean run_smoke_test(gpointer data) { "czechmap", "lityears", "litmap", "lit2map", "lit3map", "lit4map", "scimap", "chemmap", "biomap", + "matyears", "mat0map", "matmap", "mat2map", "mat3map", "mat4map", + "mat0unit1", "mat0ex1", "matunit1", "matex1", + "mat4unit8", "mat4ex8", "fyzyears", "fyzmap", "fyz2map", "fyz3map", "fyz4map", "fyzunit1", "fyzex1", "fyzunit8", "fyzex8", "fyz2unit1", "fyz2ex1", "fyz3unit1", "fyz4unit1", @@ -532,6 +535,17 @@ void activate(GtkApplication *app, gpointer user_data) { add_on3_pages(main_stack); gtk_stack_add_named(main_stack, build_on4map_page(), "on4map"); add_on4_pages(main_stack); + gtk_stack_add_named(main_stack, build_matyears_page(), "matyears"); + gtk_stack_add_named(main_stack, build_mat0map_page(), "mat0map"); + add_mat0_pages(main_stack); + gtk_stack_add_named(main_stack, build_matmap_page(), "matmap"); + add_mat_pages(main_stack); + gtk_stack_add_named(main_stack, build_mat2map_page(), "mat2map"); + add_mat2_pages(main_stack); + gtk_stack_add_named(main_stack, build_mat3map_page(), "mat3map"); + add_mat3_pages(main_stack); + gtk_stack_add_named(main_stack, build_mat4map_page(), "mat4map"); + add_mat4_pages(main_stack); gtk_stack_add_named(main_stack, build_fyzyears_page(), "fyzyears"); gtk_stack_add_named(main_stack, build_fyzmap_page(), "fyzmap"); add_fyz_pages(main_stack); diff --git a/src/mat_year0.c b/src/mat_year0.c new file mode 100644 index 0000000..ec7dbdf --- /dev/null +++ b/src/mat_year0.c @@ -0,0 +1,469 @@ +#include "graduately.h" + +NetLesson mat0_lessons[MATH_N] = { + { .n_slides = 3, .unit_page = "mat0unit1", .ex_page = "mat0ex1" }, + { .n_slides = 3, .unit_page = "mat0unit2", .ex_page = "mat0ex2" }, + { .n_slides = 3, .unit_page = "mat0unit3", .ex_page = "mat0ex3" }, + { .n_slides = 3, .unit_page = "mat0unit4", .ex_page = "mat0ex4" }, + { .n_slides = 3, .unit_page = "mat0unit5", .ex_page = "mat0ex5" }, + { .n_slides = 3, .unit_page = "mat0unit6", .ex_page = "mat0ex6" }, + { .n_slides = 3, .unit_page = "mat0unit7", .ex_page = "mat0ex7" }, + { .n_slides = 3, .unit_page = "mat0unit8", .ex_page = "mat0ex8" }, +}; + +static GtkWidget *build_mat0_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Pravidlo", "Co se počítá dřív", + "Tip: násobení a dělení jsou stejně silné a jdou zleva doprava.", + { + "Nejprve se vyřeší závorky, zevnitř ven.", + "Pak jsou mocniny a odmocniny.", + "Násobení a dělení mají přednost před sčítáním a odčítáním.", + "Sčítání a odčítání se také provádějí zleva doprava.", + NULL, + }, + }, + { + "2 / 3 • Bez závorek", "Násobení má přednost", + "Tip: 2 + 3 · 4 není 20.", + { + "Ve výrazu 2 + 3 · 4 se nejdřív násobí: 3 · 4 = 12.", + "Teprve potom se přičtou dvě: 2 + 12 = 14.", + "Ve výrazu 20 − 6 / 2 se nejdřív dělí: 6 / 2 = 3.", + "Pak 20 − 3 = 17.", + NULL, + }, + }, + { + "3 / 3 • Závorky", "Závorka změní pořadí", + "Tip: závorka se chová jako jeden počet.", + { + "(8 − 3) · 2 začíná v závorce: 5 · 2 = 10.", + "3 · (4 + 1) je 3 · 5 = 15.", + "Bez závorky by 3 · 4 + 1 vyšlo 13.", + "Závorku proto pište vždy, když má sčítání předběhnout násobení.", + NULL, + }, + }, + }; + + return math_unit_page(0, &mat0_lessons[0], "mat0_unit1", "mat0_unit1_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat0_unit1_exercise_page(void) { + static const MathItem qs[] = { + {"Spočítejte 2 + 3 · 4.", "num:14", "Násobení má přednost: 3 · 4 = 12 a 2 + 12 = 14. Výsledek je 14."}, + {"Spočítejte (8 − 3) · 2.", "num:10", "Nejdřív závorka: 8 − 3 = 5, potom 5 · 2 = 10. Výsledek je 10."}, + {"Spočítejte 20 − 6 / 2.", "num:17", "Nejdřív dělení: 6 / 2 = 3, potom 20 − 3 = 17. Výsledek je 17."}, + {"Spočítejte 3 · (4 + 1).", "num:15", "Nejdřív závorka: 4 + 1 = 5, potom 3 · 5 = 15. Výsledek je 15."}, + }; + + return math_practice_page(0, 0, "mat0unit1", + "mat0_ex1_title", "mat0_quiz1_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat0_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Součet", "Společný jmenovatel", + "Tip: zlomek v základním tvaru má nesoudělný čitatel a jmenovatel.", + { + "Sčítat a odčítat lze zlomky se stejným jmenovatelem.", + "Jinak se jmenovatelé rozšíří na nejmenší společný násobek.", + "1/2 + 1/3 má společný jmenovatel 6: 3/6 + 2/6 = 5/6.", + "3/4 − 1/4 = 2/4 = 1/2.", + NULL, + }, + }, + { + "2 / 3 • Součin", "Čitatel s čitatelem", + "Tip: před násobením jde často krátit křížem.", + { + "Zlomky se násobí čitatel čitatelem a jmenovatel jmenovatelem.", + "2/3 · 3/5 = 6/15 = 2/5.", + "Krátit se smí jen čitatel se jmenovatelem, ne dvě horní čísla mezi sebou.", + "Smíšené číslo se před počtem převede na zlomek.", + NULL, + }, + }, + { + "3 / 3 • Podíl", "Dělit znamená násobit převráceným", + "Tip: převrácený zlomek k a/b je b/a.", + { + "Dělení zlomkem je násobení jeho převrácenou hodnotou.", + "(3/4) : (1/2) = 3/4 · 2/1 = 6/4 = 3/2.", + "Výsledek se uvede v základním tvaru.", + "Nulou se dělit nesmí, proto jmenovatel nesmí vyjít 0.", + NULL, + }, + }, + }; + + return math_unit_page(0, &mat0_lessons[1], "mat0_unit2", "mat0_unit2_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat0_unit2_exercise_page(void) { + static const MathItem qs[] = { + {"Spočítejte 1/2 + 1/3. Zlomek v základním tvaru.", "num:5/6", "Společný jmenovatel je 6: 3/6 + 2/6 = 5/6. Výsledek je 5/6."}, + {"Spočítejte 3/4 − 1/4.", "num:1/2", "Jmenovatel už je stejný: 2/4 = 1/2. Výsledek je 1/2."}, + {"Spočítejte 2/3 · 3/5.", "num:2/5", "6/15 se zkrátí třemi na 2/5. Výsledek je 2/5."}, + {"Spočítejte (3/4) : (1/2).", "num:3/2", "Násobí se převráceným zlomkem: 3/4 · 2/1 = 3/2. Výsledek je 3/2."}, + }; + + return math_practice_page(0, 1, "mat0unit2", + "mat0_ex2_title", "mat0_quiz2_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat0_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Zápis", "Čárka i tečka", + "Tip: 0,5 je totéž co 1/2.", + { + "Desetinná čárka odděluje celou část od desetin, setin a tisícin.", + "0,1 je jedna desetina, 0,01 je jedna setina.", + "Při sčítání se čárky píšou pod sebe.", + "2,5 + 1,5 = 4.", + NULL, + }, + }, + { + "2 / 3 • Násobení", "Čárka se posouvá", + "Tip: násobení deseti posune čárku o jedno místo doprava.", + { + "Počet desetinných míst součinu je součet počtů míst činitelů.", + "0,2 · 5 = 1, protože dvě desetiny pětkrát jsou jedna celá.", + "1,25 − 0,5 = 0,75, což je 3/4.", + "Výsledek lze psát desetinně i zlomkem.", + NULL, + }, + }, + { + "3 / 3 • Dělení", "Dělit desetinným číslem", + "Tip: dělitel se nejdřív upraví na celé číslo.", + { + "3,6 : 0,3 se rozšíří deseti na 36 : 3 = 12.", + "Čárka se v děliteli i děleném posune o stejný počet míst.", + "Dělení deseti posune čárku o jedno místo doleva.", + "Nulou se ani u desetinných čísel dělit nesmí.", + NULL, + }, + }, + }; + + return math_unit_page(0, &mat0_lessons[2], "mat0_unit3", "mat0_unit3_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat0_unit3_exercise_page(void) { + static const MathItem qs[] = { + {"Spočítejte 2,5 + 1,5.", "num:4", "Pět desetin a pět desetin je jedna celá, celkem 4. Výsledek je 4."}, + {"Spočítejte 0,2 · 5.", "num:1", "Dvě desetiny pětkrát jsou 10 desetin, tedy 1. Výsledek je 1."}, + {"Spočítejte 3,6 : 0,3.", "num:12", "Posun čárky dá 36 : 3 = 12. Výsledek je 12."}, + {"Spočítejte 1,25 − 0,5.", "num:3/4", "1,25 − 0,50 = 0,75, tedy 3/4. Výsledek je 3/4."}, + }; + + return math_practice_page(0, 2, "mat0unit3", + "mat0_ex3_title", "mat0_quiz3_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat0_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Základ", "Jedno procento je setina", + "Tip: pište jen číslo, znak procenta ne.", + { + "1 % ze základu je jedna setina základu.", + "p % ze základu z je (p/100) · z.", + "20 % ze 150 je 0,2 · 150 = 30.", + "Základ, procentová část a počet procent jsou tři veličiny.", + NULL, + }, + }, + { + "2 / 3 • Kolik procent", "Část dělená základem", + "Tip: výsledek 0,25 znamená 25 %.", + { + "Počet procent je (část / základ) · 100.", + "15 ze 60 je 15/60 = 1/4, tedy 25 %.", + "Do odpovědi se píše 25, ne 0,25 a ne 25 %.", + "Zlomek se na procenta násobí stem.", + NULL, + }, + }, + { + "3 / 3 • Sleva a základ", "Dopočítání celku", + "Tip: sleva 10 % nechá z ceny 90 %.", + { + "Po slevě 10 % zbývá 90 % původní ceny.", + "400 − 10 % je 0,9 · 400 = 360.", + "Když 20 % základu je 18, je 1 % rovno 18/20.", + "Celý základ je pak 100 takových dílů: 90.", + NULL, + }, + }, + }; + + return math_unit_page(0, &mat0_lessons[3], "mat0_unit4", "mat0_unit4_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat0_unit4_exercise_page(void) { + static const MathItem qs[] = { + {"Kolik je 20 % ze 150? Napište jen číslo.", "num:30", "20 % je 0,2 a 0,2 · 150 = 30. Výsledek je 30."}, + {"15 je kolik procent z 60? Napište jen číslo, bez znaku %.", "num:25", "15/60 = 1/4 = 25 %. Výsledek je 25."}, + {"Cena 400 se sníží o 10 %. Jaká bude nová cena?", "num:360", "Zůstane 90 %: 0,9 · 400 = 360. Výsledek je 360."}, + {"20 % základu je 18. Jak velký je základ?", "num:90", "1 % je 18/20 = 0,9 a 100 % je 90. Výsledek je 90."}, + }; + + return math_practice_page(0, 3, "mat0unit4", + "mat0_ex4_title", "mat0_quiz4_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat0_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Poměr", "Díly celku", + "Tip: poměr 2 : 3 má dohromady 5 dílů.", + { + "Poměr a : b říká, na kolik stejných dílů se celek rozdělí.", + "Počet dílů je a + b.", + "První část je a/(a + b) z celku.", + "Z 20 v poměru 2 : 3 připadá na první část 8.", + NULL, + }, + }, + { + "2 / 3 • Úměra", "Součin vnějších a vnitřních členů", + "Tip: 3 : 5 = x : 20 znamená 3/5 = x/20.", + { + "V úměře a : b = c : d platí a · d = b · c.", + "Z 3/5 = x/20 vyjde x = 3/5 · 20 = 12.", + "Stejný poměr lze rozšiřovat i krátit.", + "Neznámý člen se dopočítá jedním násobením a jedním dělením.", + NULL, + }, + }, + { + "3 / 3 • Měřítko", "Mapa a skutečnost", + "Tip: 1 : 1000 znamená, že 1 cm na mapě je 10 m ve skutečnosti.", + { + "Měřítko 1 : k zmenšuje: skutečnost je k-krát větší než plán.", + "5 cm na mapě 1 : 1000 je 5000 cm, tedy 50 m.", + "Měřítko 5 : 1 je zvětšení, obraz je pětkrát větší.", + "4 cm v měřítku 5 : 1 odpovídá obrazu 20 cm.", + NULL, + }, + }, + }; + + return math_unit_page(0, &mat0_lessons[4], "mat0_unit5", "mat0_unit5_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat0_unit5_exercise_page(void) { + static const MathItem qs[] = { + {"Číslo 20 rozdělte v poměru 2 : 3. Jak velká je první část?", "num:8", "Dílů je 5 a první část jsou 2 z nich: 2/5 · 20 = 8. Výsledek je 8."}, + {"Na mapě 1 : 1000 jsou 5 cm. Kolik metrů je to ve skutečnosti?", "num:50", "5 cm · 1000 = 5000 cm = 50 m. Výsledek je 50."}, + {"Platí 3 : 5 = x : 20. Kolik je x?", "num:12", "x = 3/5 · 20 = 12. Výsledek je 12."}, + {"Úsečka 4 cm se zobrazí v měřítku 5 : 1. Jak dlouhý je obraz v centimetrech?", "num:20", "Měřítko 5 : 1 zvětšuje pětkrát: 4 · 5 = 20. Výsledek je 20."}, + }; + + return math_practice_page(0, 4, "mat0unit5", + "mat0_ex5_title", "mat0_quiz5_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat0_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Číselná osa", "Vpravo roste, vlevo klesá", + "Tip: přičíst kladné číslo znamená posun doprava.", + { + "Kladná čísla leží vpravo od nuly, záporná vlevo.", + "−3 + 8 je posun z −3 o 8 kroků doprava, tedy na 5.", + "Odčítání záporného čísla je totéž co přičítání kladného.", + "6 − (−2) = 6 + 2 = 8.", + NULL, + }, + }, + { + "2 / 3 • Násobení", "Znaménka", + "Tip: dvě záporná znaménka dají kladný součin.", + { + "Součin kladného a záporného čísla je záporný.", + "Součin dvou záporných čísel je kladný.", + "(−4) · (−5) = 20.", + "Stejné pravidlo platí pro dělení.", + NULL, + }, + }, + { + "3 / 3 • Absolutní hodnota", "Vzdálenost od nuly", + "Tip: absolutní hodnota nikdy není záporná.", + { + "|a| je vzdálenost čísla a od nuly na ose.", + "|−7| = 7 a |7| = 7.", + "|0| = 0.", + "Absolutní hodnota smaže znaménko, ale číslo jinak nezmění.", + NULL, + }, + }, + }; + + return math_unit_page(0, &mat0_lessons[5], "mat0_unit6", "mat0_unit6_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat0_unit6_exercise_page(void) { + static const MathItem qs[] = { + {"Spočítejte −3 + 8.", "num:5", "Z −3 se jde o 8 doprava, na 5. Výsledek je 5."}, + {"Spočítejte (−4) · (−5).", "num:20", "Součin dvou záporných čísel je kladný: 20. Výsledek je 20."}, + {"Spočítejte |−7|.", "num:7", "Vzdálenost −7 od nuly je 7. Výsledek je 7."}, + {"Spočítejte 6 − (−2).", "num:8", "Odčítat −2 znamená přičíst 2: 6 + 2 = 8. Výsledek je 8."}, + }; + + return math_practice_page(0, 5, "mat0unit6", + "mat0_ex6_title", "mat0_quiz6_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat0_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Rovnováha", "Stejně na obě strany", + "Tip: co uděláte vlevo, udělejte i vpravo.", + { + "Rovnice říká, že levá strana se rovná pravé.", + "K oběma stranám lze přičíst stejné číslo.", + "Obě strany lze vynásobit stejným nenulovým číslem.", + "Cílem je osamostatnit neznámou.", + NULL, + }, + }, + { + "2 / 3 • Kroky", "Nejdřív číslo, pak koeficient", + "Tip: 2x − 4 = 10 se nejdřív zbaví čtyřky.", + { + "x + 5 = 12 se odečtením pěti změní na x = 7.", + "3x = 15 se vydělí třemi: x = 5.", + "2x − 4 = 10 dá po přičtení čtyř 2x = 14, tedy x = 7.", + "Zkouška dosadí výsledek do původní rovnice.", + NULL, + }, + }, + { + "3 / 3 • Dělení neznámé", "Zlomek s x", + "Tip: x/2 = 6 se násobí dvěma.", + { + "x/2 je totéž co (1/2) · x.", + "Násobení obou stran dvěma dá x = 12.", + "Násobit nulou se nesmí, rovnice by ztratila význam.", + "U lineární rovnice s nenulovým koeficientem je řešení jedno.", + NULL, + }, + }, + }; + + return math_unit_page(0, &mat0_lessons[6], "mat0_unit7", "mat0_unit7_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat0_unit7_exercise_page(void) { + static const MathItem qs[] = { + {"Vyřešte x + 5 = 12. Napište x.", "num:7", "Od obou stran se odečte 5. Výsledek je 7."}, + {"Vyřešte 3x = 15. Napište x.", "num:5", "Obě strany se vydělí 3. Výsledek je 5."}, + {"Vyřešte 2x − 4 = 10. Napište x.", "num:7", "2x = 14, takže x = 7. Výsledek je 7."}, + {"Vyřešte x/2 = 6. Napište x.", "num:12", "Obě strany se násobí 2. Výsledek je 12."}, + }; + + return math_practice_page(0, 6, "mat0unit7", + "mat0_ex7_title", "mat0_quiz7_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat0_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Obdélník", "Obvod a obsah", + "Tip: obvod je součet všech stran, obsah je součin dvou sousedních.", + { + "Obdélník a × b má obvod 2 · (a + b).", + "Obdélník 5 × 3 má obvod 2 · 8 = 16.", + "Obsah je a · b. Obdélník 6 × 4 má obsah 24.", + "Jednotky obvodu jsou délkové, jednotky obsahu čtvereční.", + NULL, + }, + }, + { + "2 / 3 • Trojúhelník", "Základna a výška", + "Tip: výška je kolmá na základnu, ne délka šikmé strany.", + { + "Součet vnitřních úhlů trojúhelníku je 180°.", + "Třetí úhel je 180° minus součet dvou známých.", + "Úhly 60° a 50° doplňuje 70°.", + "Obsah je (základna · výška) / 2. Pro 8 a 5 je to 20.", + NULL, + }, + }, + { + "3 / 3 • Čtverec", "Doplnění na mřížce", + "Tip: osa y míří nahoru. Klepnutím na bod ho odeberete.", + { + "Čtverec má čtyři shodné strany a čtyři pravé úhly.", + "Druhé dva vrcholy vzniknou posunutím strany o stejnou délku kolmo.", + "U vodorovné strany leží zbývající vrcholy přímo nad ní, nebo pod ní.", + "Na mřížce jsou platné jen oba vrcholy téže strany.", + NULL, + }, + }, + }; + + return math_unit_page(0, &mat0_lessons[7], "mat0_unit8", "mat0_unit8_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat0_unit8_exercise_page(void) { + static const MathItem qs[] = { + {"Obdélník má strany 5 a 3. Jaký je jeho obvod?", "num:16", "2 · (5 + 3) = 16. Výsledek je 16."}, + {"Obdélník má strany 6 a 4. Jaký je jeho obsah?", "num:24", "Obsah je 6 · 4 = 24. Výsledek je 24."}, + {"Trojúhelník má základnu 8 a výšku 5. Jaký je jeho obsah?", "num:20", "Obsah je (8 · 5) / 2 = 20. Výsledek je 20."}, + {"Dva úhly trojúhelníku jsou 60° a 50°. Jak velký je třetí úhel ve stupních?", "num:70", "Součet je 180°, takže třetí úhel je 70°. Výsledek je 70."}, + {"Na mřížce je strana čtverce od A = [2; 3] k B = [5; 3]. Doplňte zbývající dva vrcholy.", "square:2,3;5,3", "Strana má délku 3. Druhé dva vrcholy jsou [2; 6] a [5; 6], nebo [2; 0] a [5; 0]."}, + }; + + return math_practice_page(0, 7, "mat0unit8", + "mat0_ex8_title", "mat0_quiz8_head", + qs, G_N_ELEMENTS(qs)); +} + +void add_mat0_pages(GtkStack *stack) { + typedef GtkWidget *(*MathBuilder)(void); + static const struct { + const char *unit_name; + const char *ex_name; + MathBuilder build_unit; + MathBuilder build_ex; + } pages[] = { + {"mat0unit1", "mat0ex1", build_mat0_unit1_page, build_mat0_unit1_exercise_page}, + {"mat0unit2", "mat0ex2", build_mat0_unit2_page, build_mat0_unit2_exercise_page}, + {"mat0unit3", "mat0ex3", build_mat0_unit3_page, build_mat0_unit3_exercise_page}, + {"mat0unit4", "mat0ex4", build_mat0_unit4_page, build_mat0_unit4_exercise_page}, + {"mat0unit5", "mat0ex5", build_mat0_unit5_page, build_mat0_unit5_exercise_page}, + {"mat0unit6", "mat0ex6", build_mat0_unit6_page, build_mat0_unit6_exercise_page}, + {"mat0unit7", "mat0ex7", build_mat0_unit7_page, build_mat0_unit7_exercise_page}, + {"mat0unit8", "mat0ex8", build_mat0_unit8_page, build_mat0_unit8_exercise_page}, + }; + + for (guint i = 0; i < G_N_ELEMENTS(pages); i++) { + gtk_stack_add_named(stack, pages[i].build_unit(), pages[i].unit_name); + gtk_stack_add_named(stack, pages[i].build_ex(), pages[i].ex_name); + } +} diff --git a/src/mat_year1.c b/src/mat_year1.c new file mode 100644 index 0000000..e14e245 --- /dev/null +++ b/src/mat_year1.c @@ -0,0 +1,468 @@ +#include "graduately.h" + +NetLesson mat_lessons[MATH_N] = { + { .n_slides = 3, .unit_page = "matunit1", .ex_page = "matex1" }, + { .n_slides = 3, .unit_page = "matunit2", .ex_page = "matex2" }, + { .n_slides = 3, .unit_page = "matunit3", .ex_page = "matex3" }, + { .n_slides = 3, .unit_page = "matunit4", .ex_page = "matex4" }, + { .n_slides = 3, .unit_page = "matunit5", .ex_page = "matex5" }, + { .n_slides = 3, .unit_page = "matunit6", .ex_page = "matex6" }, + { .n_slides = 3, .unit_page = "matunit7", .ex_page = "matex7" }, + { .n_slides = 3, .unit_page = "matunit8", .ex_page = "matex8" }, +}; + +static GtkWidget *build_mat_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Dělitel", "Čísla, která dělí beze zbytku", + "Tip: prvočíslo má právě dva dělitele, jedničku a sebe.", + { + "Dělitel čísla a je celé číslo, kterým a dělí beze zbytku.", + "Číslo 12 má dělitele 1, 2, 3, 4, 6 a 12, tedy šest dělitelů.", + "Prvočíslo nelze rozložit na součin dvou menších celých čísel větších než 1.", + "Jednička prvočíslo není.", + NULL, + }, + }, + { + "2 / 3 • NSD a NSN", "Společný dělitel a násobek", + "Tip: NSD · NSN dvou čísel je jejich součin.", + { + "Největší společný dělitel je největší číslo, které dělí obě čísla.", + "NSD(24, 36) = 12.", + "Nejmenší společný násobek je nejmenší kladné číslo, které je násobkem obou.", + "NSN(6, 8) = 24.", + NULL, + }, + }, + { + "3 / 3 • Rozklad", "Součin mocnin prvočísel", + "Tip: společné prvočinitele berte v nejmenší mocnině pro NSD.", + { + "Každé celé číslo větší než 1 má jediný rozklad na prvočísla.", + "72 = 2³ · 3².", + "V NSD se bere nižší exponent, v NSN vyšší.", + "Rozklad usnadní krácení zlomků i hledání společného jmenovatele.", + NULL, + }, + }, + }; + + return math_unit_page(1, &mat_lessons[0], "mat_unit1", "mat_unit1_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat_unit1_exercise_page(void) { + static const MathItem qs[] = { + {"Jaký je největší společný dělitel čísel 24 a 36?", "num:12", "Společní dělitelé jsou 1, 2, 3, 4, 6 a 12. Výsledek je 12."}, + {"Jaký je nejmenší společný násobek čísel 6 a 8?", "num:24", "Násobky 6 jsou 6, 12, 18, 24, … a 24 je první z nich dělitelné osmi. Výsledek je 24."}, + {"Spočítejte 2³ · 3².", "num:72", "8 · 9 = 72. Výsledek je 72."}, + {"Kolik kladných dělitelů má číslo 12?", "num:6", "Jsou to 1, 2, 3, 4, 6 a 12. Výsledek je 6."}, + }; + + return math_practice_page(1, 0, "matunit1", + "mat_ex1_title", "mat_quiz1_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Mocnina", "Opakované násobení", + "Tip: aⁿ je a násobené sebou n-krát.", + { + "2⁵ = 2 · 2 · 2 · 2 · 2 = 32.", + "10³ = 1000, tři nuly za jedničkou.", + "Každé číslo na první je ono samo a a⁰ = 1 pro a ≠ 0.", + "Záporný exponent znamená převrácenou hodnotu mocniny.", + NULL, + }, + }, + { + "2 / 3 • Pravidla", "Stejný základ", + "Tip: (aᵐ)ⁿ = aᵐ·ⁿ.", + { + "aᵐ · aⁿ = aᵐ⁺ⁿ.", + "aᵐ : aⁿ = aᵐ⁻ⁿ pro a ≠ 0.", + "(a · b)ⁿ = aⁿ · bⁿ.", + "(2³)² = 2⁶ = 64.", + NULL, + }, + }, + { + "3 / 3 • Odmocnina", "Druhá odmocnina", + "Tip: √a je nezáporné číslo, jehož druhá mocnina je a.", + { + "√81 = 9, protože 9² = 81.", + "√0 = 0 a √1 = 1.", + "Ze záporného čísla se v reálných číslech druhá odmocnina nedělá.", + "√(a²) = |a|, ne vždy a.", + NULL, + }, + }, + }; + + return math_unit_page(1, &mat_lessons[1], "mat_unit2", "mat_unit2_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat_unit2_exercise_page(void) { + static const MathItem qs[] = { + {"Spočítejte 2⁵.", "num:32", "Pět dvojkových činitelů je 32. Výsledek je 32."}, + {"Spočítejte 10³.", "num:1000", "10 · 10 · 10 = 1000. Výsledek je 1000."}, + {"Spočítejte √81.", "num:9", "9 · 9 = 81. Výsledek je 9."}, + {"Spočítejte (2³)².", "num:64", "(8)² = 64, nebo 2⁶ = 64. Výsledek je 64."}, + }; + + return math_practice_page(1, 1, "matunit2", + "mat_ex2_title", "mat_quiz2_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Dosazení", "Písmeno je číslo", + "Tip: nejdřív závorky, potom mocniny a násobení.", + { + "Do výrazu se za proměnnou dosadí dané číslo.", + "2(x + 3) při x = 4 je 2 · 7 = 14.", + "3x − 2x + 5 se nejdřív sloučí na x + 5.", + "Při x = 2 je x + 5 = 7.", + NULL, + }, + }, + { + "2 / 3 • Vzorce", "Druhá mocnina dvojčlenu", + "Tip: (a + b)² není a² + b².", + { + "(a + b)² = a² + 2ab + b².", + "(a − b)² = a² − 2ab + b².", + "a² − b² = (a − b)(a + b).", + "Pro a = 3 a b = 1 je (a + b)² = 16.", + NULL, + }, + }, + { + "3 / 3 • Zkrácení", "Rozdíl čtverců", + "Tip: krátit lze jen pro x, která nejsou kořenem jmenovatele.", + { + "x² − 9 = (x − 3)(x + 3).", + "(x² − 9)/(x − 3) = x + 3 pro x ≠ 3.", + "Při x = 5 je hodnota 8.", + "Dosazení x = 3 by dělilo nulou, výraz tam není definován.", + NULL, + }, + }, + }; + + return math_unit_page(1, &mat_lessons[2], "mat_unit3", "mat_unit3_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat_unit3_exercise_page(void) { + static const MathItem qs[] = { + {"Do výrazu 2(x + 3) dosažte x = 4.", "num:14", "2 · (4 + 3) = 14. Výsledek je 14."}, + {"Do výrazu (a + b)² dosažte a = 3 a b = 1.", "num:16", "4² = 16. Rozepsáno 9 + 6 + 1 = 16. Výsledek je 16."}, + {"Do výrazu 3x − 2x + 5 dosažte x = 2.", "num:7", "Výraz je x + 5, při x = 2 je 7. Výsledek je 7."}, + {"Do výrazu (x² − 9)/(x − 3) dosažte x = 5.", "num:8", "Pro x ≠ 3 je výraz roven x + 3, tedy 8. Výsledek je 8."}, + }; + + return math_practice_page(1, 2, "matunit3", + "mat_ex3_title", "mat_quiz3_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Převod", "Neznámé na jednu stranu", + "Tip: člen s x se převádí s opačným znaménkem.", + { + "2x + 1 = 9 se odečtením jedné změní na 2x = 8.", + "x = 4.", + "Členy s x patří obvykle vlevo, čísla vpravo.", + "Po každé úpravě zůstává rovnost zachovaná.", + NULL, + }, + }, + { + "2 / 3 • Obě strany", "Stejný člen vlevo i vpravo", + "Tip: 5x − 3 = 2x + 9 se zbaví 2x odečtením.", + { + "5x − 2x = 9 + 3.", + "3x = 12 a x = 4.", + "Zkouška: levá strana 5 · 4 − 3 = 17, pravá 8 + 9 = 17.", + "Když koeficient u x vyjde 0, rovnice buď nemá řešení, nebo jich má nekonečně mnoho.", + NULL, + }, + }, + { + "3 / 3 • Závorka a zlomek", "Nejdřív roznásobit", + "Tip: x/2 + 1 = 5 se nejdřív zbaví jedničky.", + { + "x/2 + 1 = 5 dá x/2 = 4, tedy x = 8.", + "3(x − 2) = 12 se vydělí třemi: x − 2 = 4.", + "Nebo se závorka roznásobí: 3x − 6 = 12.", + "Obě cesty dají x = 6.", + NULL, + }, + }, + }; + + return math_unit_page(1, &mat_lessons[3], "mat_unit4", "mat_unit4_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat_unit4_exercise_page(void) { + static const MathItem qs[] = { + {"Vyřešte 2x + 1 = 9. Napište x.", "num:4", "2x = 8, takže x = 4. Výsledek je 4."}, + {"Vyřešte 5x − 3 = 2x + 9. Napište x.", "num:4", "3x = 12, takže x = 4. Výsledek je 4."}, + {"Vyřešte x/2 + 1 = 5. Napište x.", "num:8", "x/2 = 4, takže x = 8. Výsledek je 8."}, + {"Vyřešte 3(x − 2) = 12. Napište x.", "num:6", "x − 2 = 4, takže x = 6. Výsledek je 6."}, + }; + + return math_practice_page(1, 3, "matunit4", + "mat_ex4_title", "mat_quiz4_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Dráha", "Rychlost krát čas", + "Tip: jednotky času a rychlosti musí patřit k sobě.", + { + "Při stálé rychlosti je dráha s = v · t.", + "60 km/h po dobu 2,5 h urazí 150 km.", + "2,5 h je dvě a půl hodiny, ne dvě hodiny a padesát minut.", + "Z dráhy a času se rychlost dopočítá jako s/t.", + NULL, + }, + }, + { + "2 / 3 • Práce a směs", "Sčítají se výkony, ne časy", + "Tip: kdo práci udělá za 3 hodiny, udělá za hodinu třetinu.", + { + "První pracovník udělá za hodinu 1/6 práce, druhý 1/3.", + "Společně udělají 1/6 + 2/6 = 1/2 práce za hodinu, tedy celou za 2 hodiny.", + "U směsi se sčítá množství čisté látky.", + "2 kg dvacetiprocentní a 3 kg desetiprocentní směsi obsahují 0,4 + 0,3 = 0,7 kg, tedy 14 %.", + NULL, + }, + }, + { + "3 / 3 • Sleva", "Nová cena je část základu", + "Tip: po slevě 20 % zbývá 80 % původní ceny.", + { + "Když je nová cena 160 a sleva byla 20 %, je 160 rovno 80 % původní ceny.", + "Původní cena je 160 / 0,8 = 200.", + "Sleva v korunách je pak 40.", + "Nepleťte si slevu 20 % s tím, že se k nové ceně přičte 20 %.", + NULL, + }, + }, + }; + + return math_unit_page(1, &mat_lessons[4], "mat_unit5", "mat_unit5_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat_unit5_exercise_page(void) { + static const MathItem qs[] = { + {"Auto jede 2,5 hodiny rychlostí 60 km/h. Kolik kilometrů ujede?", "num:150", "s = v · t = 60 · 2,5 = 150. Výsledek je 150."}, + {"První pracovník práci udělá za 6 hodin, druhý za 3 hodiny. Za kolik hodin ji udělají společně?", "num:2", "Za hodinu spolu udělají 1/2 práce, celou tedy za 2 hodiny. Výsledek je 2."}, + {"Smíchají se 2 kg směsi s 20 % látky a 3 kg směsi s 10 % látky. Kolik procent látky má směs? Napište jen číslo.", "num:14", "Čisté látky je 0,7 kg z 5 kg, tedy 14 %. Výsledek je 14."}, + {"Po slevě 20 % stojí zboží 160. Jaká byla původní cena?", "num:200", "160 je 80 % původní ceny, základ je 200. Výsledek je 200."}, + }; + + return math_practice_page(1, 4, "matunit5", + "mat_ex5_title", "mat_quiz5_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Trojúhelník", "Součet 180°", + "Tip: rovnoramenný trojúhelník má dvě ramena a dva úhly u základny shodné.", + { + "Součet vnitřních úhlů každého trojúhelníku je 180°.", + "V rovnoramenném trojúhelníku jsou úhly při základně shodné.", + "Když jsou úhly při základně 70°, je úhel proti základně 40°.", + "Rovnostranný trojúhelník má všechny strany shodné a každý úhel 60°.", + NULL, + }, + }, + { + "2 / 3 • Vnější úhel", "Součet dvou vzdálených vnitřních", + "Tip: vnější úhel a sousední vnitřní úhel dávají 180°.", + { + "Vnější úhel vznikne prodloužením jedné strany.", + "Je roven součtu dvou vnitřních úhlů, které s ním nesousedí.", + "Vnitřní úhly 40° a 60° dávají vnější úhel 100° u zbývajícího vrcholu.", + "Součet vnějšího úhlu a přilehlého vnitřního je 180°.", + NULL, + }, + }, + { + "3 / 3 • Mnohoúhelník", "Pravidelný šestiúhelník", + "Tip: součet vnitřních úhlů n-úhelníku je (n − 2) · 180°.", + { + "Trojúhelník má součet 180°, čtyřúhelník 360°.", + "Obecný vzorec je (n − 2) · 180°.", + "Pravidelný mnohoúhelník má všechny strany i úhly shodné.", + "Pravidelný šestiúhelník má vnitřní úhel 720° / 6 = 120°.", + NULL, + }, + }, + }; + + return math_unit_page(1, &mat_lessons[5], "mat_unit6", "mat_unit6_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat_unit6_exercise_page(void) { + static const MathItem qs[] = { + {"Vnější úhel trojúhelníku je součet vnitřních úhlů 40° a 60°. Jak je velký ve stupních?", "num:100", "Vnější úhel se rovná součtu dvou nesousedních vnitřních úhlů: 100°. Výsledek je 100."}, + {"Rovnoramenný trojúhelník má úhly při základně 70°. Jak velký je úhel proti základně?", "num:40", "180° − 140° = 40°. Výsledek je 40."}, + {"Rovnostranný trojúhelník má stranu 6. Jaký je jeho obvod?", "num:18", "Tři shodné strany: 3 · 6 = 18. Výsledek je 18."}, + {"Jak velký je jeden vnitřní úhel pravidelného šestiúhelníku ve stupních?", "num:120", "Součet je (6 − 2) · 180° = 720° a jeden úhel je 120°. Výsledek je 120."}, + }; + + return math_practice_page(1, 5, "matunit6", + "mat_ex6_title", "mat_quiz6_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Kružnice", "Obvod a obsah", + "Tip: u výsledku s π nechte π ve výrazu a pište ho jako pi.", + { + "Kružnice s poloměrem r má obvod 2πr.", + "Obsah kruhu je πr².", + "Pro r = 7 je obvod 14π.", + "Pro r = 3 je obsah 9π.", + NULL, + }, + }, + { + "2 / 3 • Lichoběžník", "Dvě základny a výška", + "Tip: obsah je průměr základen krát výška.", + { + "Lichoběžník má právě jednu dvojici rovnoběžných stran, základny a a c.", + "Obsah je (a + c) · v / 2.", + "Základny 4 a 6 a výška 3 dávají obsah 15.", + "Výška je kolmá vzdálenost základen.", + NULL, + }, + }, + { + "3 / 3 • Pythagoras", "Pravoúhlý trojúhelník", + "Tip: přepona je nejdelší strana a leží proti pravému úhlu.", + { + "V pravoúhlém trojúhelníku je a² + b² = c².", + "c je přepona, a a b jsou odvěsny.", + "Trojúhelník 5, 12, 13 je pravoúhlý, protože 25 + 144 = 169.", + "Přepona je 13.", + NULL, + }, + }, + }; + + return math_unit_page(1, &mat_lessons[6], "mat_unit7", "mat_unit7_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat_unit7_exercise_page(void) { + static const MathItem qs[] = { + {"Jaký je obvod kružnice s poloměrem 7? Nechte výsledek v násobcích π.", "num:14*pi", "Obvod je 2πr = 14π. Výsledek je 14π."}, + {"Jaký je obsah kruhu s poloměrem 3? Nechte výsledek v násobcích π.", "num:9*pi", "Obsah je πr² = 9π. Výsledek je 9π."}, + {"Lichoběžník má základny 4 a 6 a výšku 3. Jaký je jeho obsah?", "num:15", "(4 + 6) · 3 / 2 = 15. Výsledek je 15."}, + {"Pravoúhlý trojúhelník má odvěsny 5 a 12. Jak dlouhá je přepona?", "num:13", "5² + 12² = 25 + 144 = 169 = 13². Výsledek je 13."}, + }; + + return math_practice_page(1, 6, "matunit7", + "mat_ex7_title", "mat_quiz7_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Body", "Vzdálenost na mřížce", + "Tip: u svislé nebo vodorovné úsečky stačí rozdíl souřadnic.", + { + "Bod v rovině má souřadnice [x; y].", + "Vzdálenost [x₁; y₁] a [x₂; y₂] je √((x₂ − x₁)² + (y₂ − y₁)²).", + "A = [1; 1] a B = [5; 1] mají vzdálenost 4.", + "Osa y na naší mřížce míří nahoru.", + NULL, + }, + }, + { + "2 / 3 • SSS", "Trojúhelník ze tří stran", + "Tip: třetí vrchol je průsečík dvou kružnic.", + { + "Konstrukce SSS použije tři strany.", + "Z bodu A se rýsuje kružnice o poloměru |AC|, z bodu B o poloměru |BC|.", + "Kružnice se protnou ve dvou bodech, pokud trojúhelník existuje.", + "Trojúhelníková nerovnost: součet dvou stran je větší než třetí.", + NULL, + }, + }, + { + "3 / 3 • Příklad", "Strany 4, 3 a 5", + "Tip: 3² + 4² = 5², pravý úhel je u bodu A.", + { + "A = [1; 1], B = [5; 1], takže |AB| = 4.", + "Hledáme C tak, aby |AC| = 3 a |BC| = 5.", + "Na mřížce od 0 do 10 a od 0 do 8 leží jen C = [1; 4].", + "Druhý průsečík [1; −2] je pod mřížkou.", + NULL, + }, + }, + }; + + return math_unit_page(1, &mat_lessons[7], "mat_unit8", "mat_unit8_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat_unit8_exercise_page(void) { + static const MathItem qs[] = { + {"Jaká je vzdálenost bodů A = [1; 1] a B = [5; 1]?", "num:4", "Body mají stejné y, vzdálenost je 5 − 1 = 4. Výsledek je 4."}, + {"Jaký je obsah pravoúhlého trojúhelníku s odvěsnami 3 a 4?", "num:6", "Obsah je (3 · 4) / 2 = 6. Výsledek je 6."}, + {"Jaký je obvod trojúhelníku se stranami 3, 4 a 5?", "num:12", "3 + 4 + 5 = 12. Výsledek je 12."}, + {"A = [1; 1] a B = [5; 1] jsou dané. Zakreslete C, pro které je |AC| = 3 a |BC| = 5.", "points:1,4;fix:1,1;fix:5,1", "Kružnice se na mřížce protnou v [1; 4]. Bod [1; −2] už na ní není."}, + }; + + return math_practice_page(1, 7, "matunit8", + "mat_ex8_title", "mat_quiz8_head", + qs, G_N_ELEMENTS(qs)); +} + +void add_mat_pages(GtkStack *stack) { + typedef GtkWidget *(*MathBuilder)(void); + static const struct { + const char *unit_name; + const char *ex_name; + MathBuilder build_unit; + MathBuilder build_ex; + } pages[] = { + {"matunit1", "matex1", build_mat_unit1_page, build_mat_unit1_exercise_page}, + {"matunit2", "matex2", build_mat_unit2_page, build_mat_unit2_exercise_page}, + {"matunit3", "matex3", build_mat_unit3_page, build_mat_unit3_exercise_page}, + {"matunit4", "matex4", build_mat_unit4_page, build_mat_unit4_exercise_page}, + {"matunit5", "matex5", build_mat_unit5_page, build_mat_unit5_exercise_page}, + {"matunit6", "matex6", build_mat_unit6_page, build_mat_unit6_exercise_page}, + {"matunit7", "matex7", build_mat_unit7_page, build_mat_unit7_exercise_page}, + {"matunit8", "matex8", build_mat_unit8_page, build_mat_unit8_exercise_page}, + }; + + for (guint i = 0; i < G_N_ELEMENTS(pages); i++) { + gtk_stack_add_named(stack, pages[i].build_unit(), pages[i].unit_name); + gtk_stack_add_named(stack, pages[i].build_ex(), pages[i].ex_name); + } +} diff --git a/src/mat_year2.c b/src/mat_year2.c new file mode 100644 index 0000000..91df414 --- /dev/null +++ b/src/mat_year2.c @@ -0,0 +1,468 @@ +#include "graduately.h" + +NetLesson mat2_lessons[MATH_N] = { + { .n_slides = 3, .unit_page = "mat2unit1", .ex_page = "mat2ex1" }, + { .n_slides = 3, .unit_page = "mat2unit2", .ex_page = "mat2ex2" }, + { .n_slides = 3, .unit_page = "mat2unit3", .ex_page = "mat2ex3" }, + { .n_slides = 3, .unit_page = "mat2unit4", .ex_page = "mat2ex4" }, + { .n_slides = 3, .unit_page = "mat2unit5", .ex_page = "mat2ex5" }, + { .n_slides = 3, .unit_page = "mat2unit6", .ex_page = "mat2ex6" }, + { .n_slides = 3, .unit_page = "mat2unit7", .ex_page = "mat2ex7" }, + { .n_slides = 3, .unit_page = "mat2unit8", .ex_page = "mat2ex8" }, +}; + +static GtkWidget *build_mat2_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Předpis", "y = ax + b", + "Tip: a je směrnice, b je úsek na ose y.", + { + "Lineární funkce má předpis f(x) = ax + b.", + "Grafem je přímka.", + "b je hodnota f(0), tedy průsečík s osou y.", + "Pro f(x) = 2x − 3 je průsečík s osou y číslo −3.", + NULL, + }, + }, + { + "2 / 3 • Směrnice", "O kolik y vzroste", + "Tip: směrnice je podíl změny y a změny x.", + { + "Mezi body [x₁; y₁] a [x₂; y₂] je a = (y₂ − y₁)/(x₂ − x₁).", + "Body [1; 2] a [3; 8] dávají směrnici (8 − 2)/(3 − 1) = 3.", + "Kladná směrnice znamená růst, záporná pokles.", + "Nulová směrnice je vodorovná přímka.", + NULL, + }, + }, + { + "3 / 3 • Hodnota a kořen", "Dosazení a rovnice", + "Tip: nulový bod je x, pro které je f(x) = 0.", + { + "f(4) u funkce 2x − 3 je 8 − 3 = 5.", + "Rovnice 2x − 6 = 0 má řešení x = 3.", + "To je průsečík přímky y = 2x − 6 s osou x.", + "Lineární funkce s a ≠ 0 má právě jeden nulový bod.", + NULL, + }, + }, + }; + + return math_unit_page(2, &mat2_lessons[0], "mat2_unit1", "mat2_unit1_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat2_unit1_exercise_page(void) { + static const MathItem qs[] = { + {"Funkce f(x) = 2x − 3. Kolik je f(4)?", "num:5", "2 · 4 − 3 = 5. Výsledek je 5."}, + {"Jaký je průsečík funkce f(x) = 2x − 3 s osou y?", "num:-3", "f(0) = −3. Výsledek je -3."}, + {"Jaká je směrnice přímky body [1; 2] a [3; 8]?", "num:3", "(8 − 2)/(3 − 1) = 3. Výsledek je 3."}, + {"Vyřešte 2x − 6 = 0. Napište x.", "num:3", "2x = 6, takže x = 3. Výsledek je 3."}, + }; + + return math_practice_page(2, 0, "mat2unit1", + "mat2_ex1_title", "mat2_quiz1_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat2_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Dvojice", "Jedno x a jedno y", + "Tip: výsledek pište x;y, nejdřív x.", + { + "Soustava dvou lineárních rovnic hledá dvojici [x; y], která vyhovuje oběma.", + "Řešením je průsečík dvou přímek.", + "Sčítací metoda sečte rovnice tak, aby jedna neznámá zmizela.", + "Dosazovací metoda vyjádří jednu neznámou a dosadí ji do druhé rovnice.", + NULL, + }, + }, + { + "2 / 3 • Sčítání", "Opačné koeficienty", + "Tip: x + y = 10 a x − y = 2 se sčítají na 2x = 12.", + { + "Součet dá 2x = 12, tedy x = 6.", + "Z x + y = 10 je y = 4.", + "Zkouška ve druhé rovnici: 6 − 4 = 2.", + "Odečtení rovnic se použije, když má neznámá v obou stejný koeficient.", + NULL, + }, + }, + { + "3 / 3 • Odečtení", "Stejný koeficient", + "Tip: rovnice lze před odečtením násobit.", + { + "2x + y = 8 a x + y = 5 se odečtou na x = 3.", + "Pak y = 2.", + "3x + y = 10 a x + y = 6 dají 2x = 4, tedy x = 2 a y = 4.", + "x + y = 7 a x − y = 1 dají x = 4 a y = 3.", + NULL, + }, + }, + }; + + return math_unit_page(2, &mat2_lessons[1], "mat2_unit2", "mat2_unit2_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat2_unit2_exercise_page(void) { + static const MathItem qs[] = { + {"Vyřešte soustavu x + y = 10 a x − y = 2. Napište x a y oddělené středníkem.", "pair:6;4", "Sečtení dá 2x = 12, x = 6 a y = 4. Napište 6;4."}, + {"Vyřešte soustavu 2x + y = 8 a x + y = 5. Napište x a y oddělené středníkem.", "pair:3;2", "Odečtení dá x = 3 a y = 2. Napište 3;2."}, + {"Vyřešte soustavu x + y = 7 a x − y = 1. Napište x a y oddělené středníkem.", "pair:4;3", "Sečtení dá 2x = 8, x = 4 a y = 3. Napište 4;3."}, + {"Vyřešte soustavu 3x + y = 10 a x + y = 6. Napište x a y oddělené středníkem.", "pair:2;4", "Odečtení dá 2x = 4, x = 2 a y = 4. Napište 2;4."}, + }; + + return math_practice_page(2, 1, "mat2unit2", + "mat2_ex2_title", "mat2_quiz2_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat2_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Tvar", "ax² + bx + c = 0", + "Tip: kořeny pište oddělené středníkem, pořadí nehraje roli.", + { + "Kvadratická rovnice má a ≠ 0.", + "Kořeny rovnice x² − 5x + 6 = 0 jsou 2 a 3, protože (x − 2)(x − 3) = 0.", + "x² − 4 = 0 má kořeny −2 a 2.", + "Součin je nula, právě když je nula aspoň jeden činitel.", + NULL, + }, + }, + { + "2 / 3 • Diskriminant", "D = b² − 4ac", + "Tip: D > 0 znamená dva různé reálné kořeny.", + { + "Pro ax² + bx + c = 0 je D = b² − 4ac.", + "U x² − 2x − 3 je a = 1, b = −2, c = −3 a D = 4 + 12 = 16.", + "D = 0 znamená jeden dvojnásobný kořen.", + "D < 0 znamená, že v reálných číslech řešení není.", + NULL, + }, + }, + { + "3 / 3 • Vzorec", "Kořen je (−b ± √D) / (2a)", + "Tip: dvojnásobný kořen x² + 6x + 9 = 0 je −3.", + { + "x = (−b ± √D) / (2a).", + "x² + 6x + 9 = (x + 3)², takže kořen −3 je dvojnásobný.", + "U dvojnásobného kořene stačí napsat číslo jednou.", + "Zkouška dosadí kořen zpět do rovnice.", + NULL, + }, + }, + }; + + return math_unit_page(2, &mat2_lessons[2], "mat2_unit3", "mat2_unit3_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat2_unit3_exercise_page(void) { + static const MathItem qs[] = { + {"Najděte kořeny rovnice x² − 5x + 6 = 0. Oddělte je středníkem.", "set:2;3", "(x − 2)(x − 3) = 0. Napište 2;3."}, + {"Najděte kořeny rovnice x² − 4 = 0. Oddělte je středníkem.", "set:-2;2", "x² = 4, takže x = −2 nebo x = 2. Napište -2;2."}, + {"Jaký je diskriminant rovnice x² − 2x − 3 = 0?", "num:16", "D = (−2)² − 4 · 1 · (−3) = 4 + 12 = 16. Výsledek je 16."}, + {"Jaký je dvojnásobný kořen rovnice x² + 6x + 9 = 0?", "num:-3", "(x + 3)² = 0, kořen je −3. Výsledek je -3."}, + }; + + return math_practice_page(2, 2, "mat2unit3", + "mat2_ex3_title", "mat2_quiz3_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat2_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Obor", "Jmenovatel nesmí být nula", + "Tip: podmínku pište dřív, než krátíte.", + { + "Lomený výraz je podíl dvou mnohočlenů.", + "Není definován tam, kde je jmenovatel nula.", + "(x + 2)/(x − 2) nemá smysl pro x = 2.", + "Po krácení se vyloučené hodnoty do oboru nevracejí.", + NULL, + }, + }, + { + "2 / 3 • Krácení", "Rozdíl čtverců", + "Tip: x² − 1 = (x − 1)(x + 1).", + { + "(x² − 1)/(x − 1) = x + 1 pro x ≠ 1.", + "Při x = 4 je hodnota 5.", + "(x² − 4)/(x + 2) = x − 2 pro x ≠ −2.", + "Při x = 5 je hodnota 3.", + NULL, + }, + }, + { + "3 / 3 • Násobení", "Čitatel s čitatelem", + "Tip: x ve jmenovateli se zkrátí jen pro x ≠ 0.", + { + "Zlomky se násobí stejně jako číselné.", + "(2x/4) · (6/x) = 12x/(4x) = 3 pro x ≠ 0.", + "6x/(3x) = 2 pro x ≠ 0.", + "Než se zkrátí písmeno, ověří se, že není nula.", + NULL, + }, + }, + }; + + return math_unit_page(2, &mat2_lessons[3], "mat2_unit4", "mat2_unit4_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat2_unit4_exercise_page(void) { + static const MathItem qs[] = { + {"Do výrazu (x² − 1)/(x − 1) dosažte x = 4.", "num:5", "Pro x ≠ 1 je výraz x + 1, tedy 5. Výsledek je 5."}, + {"Pro které x nemá výraz (x + 2)/(x − 2) smysl?", "num:2", "Jmenovatel x − 2 je nula právě při x = 2. Výsledek je 2."}, + {"Upravte (2x/4) · (6/x) pro x ≠ 0.", "num:3", "12x/(4x) = 3. Výsledek je 3."}, + {"Do výrazu (x² − 4)/(x + 2) dosažte x = 5.", "num:3", "Pro x ≠ −2 je výraz x − 2, tedy 3. Výsledek je 3."}, + }; + + return math_practice_page(2, 3, "mat2unit4", + "mat2_ex4_title", "mat2_quiz4_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat2_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Odmocnina jako exponent", "a^(1/n)", + "Tip: a^(1/2) je druhá odmocnina z a.", + { + "Pro a > 0 je a^(1/n) n-tá odmocnina z a.", + "16^(1/2) = 4.", + "a^(m/n) je n-tá odmocnina z aᵐ, nebo m-tá mocnina odmocniny.", + "8^(2/3) = (8^(1/3))² = 2² = 4.", + NULL, + }, + }, + { + "2 / 3 • Stejný základ", "Exponenty se sčítají", + "Tip: 3² · 3³ = 3⁵.", + { + "aᵐ · aⁿ = aᵐ⁺ⁿ.", + "3² · 3³ = 3⁵ = 243.", + "(aᵐ)ⁿ = aᵐ·ⁿ.", + "Základ musí být při sčítání exponentů stejný.", + NULL, + }, + }, + { + "3 / 3 • Záporný exponent", "Převrácená hodnota", + "Tip: a⁻ⁿ = 1/aⁿ pro a ≠ 0.", + { + "10⁻² = 1/100.", + "10⁻¹ = 0,1 a 10⁰ = 1.", + "Záporný exponent nepřevrací znaménko základu, ale celou mocninu.", + "(−2)⁻² = 1/4, protože (−2)² = 4.", + NULL, + }, + }, + }; + + return math_unit_page(2, &mat2_lessons[4], "mat2_unit5", "mat2_unit5_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat2_unit5_exercise_page(void) { + static const MathItem qs[] = { + {"Spočítejte 16^(1/2).", "num:4", "Druhá odmocnina z 16 je 4. Výsledek je 4."}, + {"Spočítejte 8^(2/3).", "num:4", "Třetí odmocnina z 8 je 2 a 2² = 4. Výsledek je 4."}, + {"Spočítejte 3² · 3³.", "num:243", "3⁵ = 243. Výsledek je 243."}, + {"Spočítejte 10⁻².", "num:1/100", "10⁻² = 1/10² = 1/100. Výsledek je 1/100."}, + }; + + return math_practice_page(2, 4, "mat2unit5", + "mat2_ex5_title", "mat2_quiz5_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat2_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Poměry", "Odvěsny a přepona", + "Tip: sinus je protilehlá odvěsna ku přeponě.", + { + "V pravoúhlém trojúhelníku je sin α = protilehlá / přepona.", + "cos α = přilehlá / přepona.", + "tan α = protilehlá / přilehlá.", + "Přepona je strana proti pravému úhlu.", + NULL, + }, + }, + { + "2 / 3 • Tabulkové úhly", "30°, 45° a 60°", + "Tip: sinus 30° je 1/2.", + { + "sin 30° = 1/2 a cos 60° = 1/2.", + "sin 60° = cos 30° = √3/2.", + "sin 45° = cos 45° = √2/2.", + "tan 45° = 1.", + NULL, + }, + }, + { + "3 / 3 • Dopočet strany", "Přepona a úhel", + "Tip: proti úhlu 30° je polovina přepony.", + { + "Známe-li přeponu a úhel, protilehlá odvěsna je přepona · sin α.", + "Proti 30° při přeponě 10 je odvěsna 5.", + "Přilehlá odvěsna by byla 10 · cos 30°.", + "Tangens se hodí, když jsou známé dvě odvěsny a ne přepona.", + NULL, + }, + }, + }; + + return math_unit_page(2, &mat2_lessons[5], "mat2_unit6", "mat2_unit6_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat2_unit6_exercise_page(void) { + static const MathItem qs[] = { + {"Kolik je sin 30°?", "num:1/2", "Sinus 30° je 1/2. Výsledek je 1/2."}, + {"Kolik je cos 60°?", "num:1/2", "Kosinus 60° je 1/2. Výsledek je 1/2."}, + {"Kolik je tan 45°?", "num:1", "Protilehlá a přilehlá odvěsna jsou u 45° stejně dlouhé. Výsledek je 1."}, + {"Přepona pravoúhlého trojúhelníku je 10 a jeden úhel je 30°. Jak dlouhá je odvěsna proti tomuto úhlu?", "num:5", "10 · sin 30° = 10 · 1/2 = 5. Výsledek je 5."}, + }; + + return math_practice_page(2, 5, "mat2unit6", + "mat2_ex6_title", "mat2_quiz6_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat2_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Pythagoras", "a² + b² = c²", + "Tip: 8² + 15² = 64 + 225 = 289 = 17².", + { + "V pravoúhlém trojúhelníku se součet čtverců nad odvěsnami rovná čtverci nad přeponou.", + "Trojúhelník 8, 15, 17 je pravoúhlý a přepona je 17.", + "Čtverec nad přeponou trojúhelníku 3, 4, 5 má obsah 25.", + "Věta platí jen v pravoúhlém trojúhelníku.", + NULL, + }, + }, + { + "2 / 3 • Eukleides", "Úsek přepony", + "Tip: a² = c · a_c, kde a_c je úsek přepony přilehlý k odvěsně a.", + { + "Odvěsna je geometrický průměr přepony a svého úseku.", + "a² = c · a_c a b² = c · b_c.", + "Úseky a_c a b_c dají dohromady přeponu c.", + "U odvěsen 6 a 8 je přepona 10 a úsek u odvěsny 6 je 36/10 = 18/5.", + NULL, + }, + }, + { + "3 / 3 • Výška", "v² = a_c · b_c", + "Tip: také platí a · b = c · v.", + { + "Výška k přeponě je geometrický průměr obou úseků.", + "Z a · b = c · v plyne v = ab/c.", + "Pro odvěsny 6 a 8 a přeponu 10 je v = 48/10 = 24/5.", + "Obsah trojúhelníku je pak (c · v) / 2, stejně jako (a · b) / 2.", + NULL, + }, + }, + }; + + return math_unit_page(2, &mat2_lessons[6], "mat2_unit7", "mat2_unit7_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat2_unit7_exercise_page(void) { + static const MathItem qs[] = { + {"Pravoúhlý trojúhelník má odvěsny 8 a 15. Jak dlouhá je přepona?", "num:17", "64 + 225 = 289 = 17². Výsledek je 17."}, + {"Jaký obsah má čtverec sestrojený nad přeponou pravoúhlého trojúhelníku se stranami 3, 4 a 5?", "num:25", "Přepona je 5 a 5² = 25. Výsledek je 25."}, + {"Odvěsny jsou 6 a 8, přepona je 10. Jak dlouhý je úsek přepony přilehlý k odvěsně 6?", "num:18/5", "a² = c · a_c, takže a_c = 36/10 = 18/5. Výsledek je 18/5."}, + {"Odvěsny jsou 6 a 8, přepona je 10. Jak dlouhá je výška k přeponě?", "num:24/5", "v = ab/c = 48/10 = 24/5. Výsledek je 24/5."}, + }; + + return math_practice_page(2, 6, "mat2unit7", + "mat2_ex7_title", "mat2_quiz7_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat2_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Úhly", "Obvodový je polovina středového", + "Tip: oba úhly musí být nad stejným obloukem.", + { + "Středový úhel má vrchol ve středu kružnice.", + "Obvodový úhel má vrchol na kružnici.", + "Nad stejným obloukem je obvodový úhel polovinou středového.", + "Středový úhel 80° patří k obvodovému úhlu 40°.", + NULL, + }, + }, + { + "2 / 3 • Oblouk", "Část obvodu", + "Tip: délka oblouku je (α/360°) · 2πr.", + { + "Celý obvod je 2πr.", + "Oblouk 60° je šestina obvodu.", + "Při r = 6 je jeho délka 2π.", + "Výsledek nechte v násobcích π.", + NULL, + }, + }, + { + "3 / 3 • Thales", "Pravý úhel nad průměrem", + "Tip: vrchol pravého úhlu leží na kružnici s průměrem AB.", + { + "Thaletova věta: úhel nad průměrem je pravý.", + "Je-li AB průměr, je úhel ACB pravý pro každé C na kružnici různé od A a B.", + "Na mřížce má průměr od [2; 4] do [8; 4] střed [5; 4] a poloměr 3.", + "Vyhovují body [5; 7] a [5; 1].", + NULL, + }, + }, + }; + + return math_unit_page(2, &mat2_lessons[7], "mat2_unit8", "mat2_unit8_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat2_unit8_exercise_page(void) { + static const MathItem qs[] = { + {"Středový úhel je 80°. Jak velký je obvodový úhel nad stejným obloukem ve stupních?", "num:40", "Obvodový úhel je polovina středového. Výsledek je 40."}, + {"Kružnice má poloměr 6. Jak dlouhý je oblouk 60°? Nechte π ve výsledku.", "num:2*pi", "(60/360) · 2π · 6 = 2π. Výsledek je 2π."}, + {"Obvodový úhel je 35°. Jak velký je středový úhel nad stejným obloukem ve stupních?", "num:70", "Středový úhel je dvojnásobek obvodového. Výsledek je 70."}, + {"AB je průměr od [2; 4] do [8; 4]. Zakreslete bod C na kružnici, ve kterém je úhel ACB pravý.", "right:2,4;8,4", "Thaletova kružnice má střed [5; 4] a poloměr 3. Na mřížce vyhovuje [5; 7] nebo [5; 1]."}, + }; + + return math_practice_page(2, 7, "mat2unit8", + "mat2_ex8_title", "mat2_quiz8_head", + qs, G_N_ELEMENTS(qs)); +} + +void add_mat2_pages(GtkStack *stack) { + typedef GtkWidget *(*MathBuilder)(void); + static const struct { + const char *unit_name; + const char *ex_name; + MathBuilder build_unit; + MathBuilder build_ex; + } pages[] = { + {"mat2unit1", "mat2ex1", build_mat2_unit1_page, build_mat2_unit1_exercise_page}, + {"mat2unit2", "mat2ex2", build_mat2_unit2_page, build_mat2_unit2_exercise_page}, + {"mat2unit3", "mat2ex3", build_mat2_unit3_page, build_mat2_unit3_exercise_page}, + {"mat2unit4", "mat2ex4", build_mat2_unit4_page, build_mat2_unit4_exercise_page}, + {"mat2unit5", "mat2ex5", build_mat2_unit5_page, build_mat2_unit5_exercise_page}, + {"mat2unit6", "mat2ex6", build_mat2_unit6_page, build_mat2_unit6_exercise_page}, + {"mat2unit7", "mat2ex7", build_mat2_unit7_page, build_mat2_unit7_exercise_page}, + {"mat2unit8", "mat2ex8", build_mat2_unit8_page, build_mat2_unit8_exercise_page}, + }; + + for (guint i = 0; i < G_N_ELEMENTS(pages); i++) { + gtk_stack_add_named(stack, pages[i].build_unit(), pages[i].unit_name); + gtk_stack_add_named(stack, pages[i].build_ex(), pages[i].ex_name); + } +} diff --git a/src/mat_year3.c b/src/mat_year3.c new file mode 100644 index 0000000..04e3bae --- /dev/null +++ b/src/mat_year3.c @@ -0,0 +1,468 @@ +#include "graduately.h" + +NetLesson mat3_lessons[MATH_N] = { + { .n_slides = 3, .unit_page = "mat3unit1", .ex_page = "mat3ex1" }, + { .n_slides = 3, .unit_page = "mat3unit2", .ex_page = "mat3ex2" }, + { .n_slides = 3, .unit_page = "mat3unit3", .ex_page = "mat3ex3" }, + { .n_slides = 3, .unit_page = "mat3unit4", .ex_page = "mat3ex4" }, + { .n_slides = 3, .unit_page = "mat3unit5", .ex_page = "mat3ex5" }, + { .n_slides = 3, .unit_page = "mat3unit6", .ex_page = "mat3ex6" }, + { .n_slides = 3, .unit_page = "mat3unit7", .ex_page = "mat3ex7" }, + { .n_slides = 3, .unit_page = "mat3unit8", .ex_page = "mat3ex8" }, +}; + +static GtkWidget *build_mat3_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Parabola", "f(x) = ax² + bx + c", + "Tip: pro a > 0 má parabola minimum, pro a < 0 maximum.", + { + "Grafem kvadratické funkce je parabola.", + "Průsečík s osou y je bod [0; c].", + "U f(x) = x² − 4x + 3 je f(0) = 3.", + "Osa paraboly je svislá.", + NULL, + }, + }, + { + "2 / 3 • Vrchol", "x = −b/(2a)", + "Tip: souřadnice vrcholu pište x;y.", + { + "x-ová souřadnice vrcholu je −b/(2a).", + "U x² − 4x + 3 je to −(−4)/2 = 2.", + "f(2) = 4 − 8 + 3 = −1.", + "Vrchol je tedy [2; −1].", + NULL, + }, + }, + { + "3 / 3 • Kořeny", "Průsečíky s osou x", + "Tip: kořeny jsou řešení f(x) = 0.", + { + "x² − 4x + 3 = (x − 1)(x − 3).", + "Kořeny jsou 1 a 3.", + "Leží symetricky kolem osy paraboly.", + "f(4) = 16 − 16 + 3 = 3, stejně jako f(0).", + NULL, + }, + }, + }; + + return math_unit_page(3, &mat3_lessons[0], "mat3_unit1", "mat3_unit1_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat3_unit1_exercise_page(void) { + static const MathItem qs[] = { + {"Najděte vrchol funkce f(x) = x² − 4x + 3. Napište x a y oddělené středníkem.", "pair:2;-1", "x = 4/2 = 2 a f(2) = −1. Napište 2;-1."}, + {"Najděte kořeny funkce f(x) = x² − 4x + 3. Oddělte je středníkem.", "set:1;3", "(x − 1)(x − 3) = 0. Napište 1;3."}, + {"Kolik je f(0) u funkce f(x) = x² − 4x + 3?", "num:3", "f(0) = 3, to je absolutní člen. Výsledek je 3."}, + {"Kolik je f(4) u funkce f(x) = x² − 4x + 3?", "num:3", "16 − 16 + 3 = 3. Výsledek je 3."}, + }; + + return math_practice_page(3, 0, "mat3unit1", + "mat3_ex1_title", "mat3_quiz1_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat3_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Definice", "Logaritmus je exponent", + "Tip: log_b(x) = y právě tehdy, když bʸ = x.", + { + "Základ logaritmu je kladný a různý od 1.", + "Argument logaritmu je kladný.", + "log₁₀(1000) = 3, protože 10³ = 1000.", + "log₂(32) = 5, protože 2⁵ = 32.", + NULL, + }, + }, + { + "2 / 3 • Rovnice", "Stejný základ", + "Tip: 2ˣ = 16 se přepíše jako 2ˣ = 2⁴.", + { + "Exponentialní rovnice se stejným základem porovná exponenty.", + "2ˣ = 16 má řešení x = 4.", + "log₃(81) = 4, protože 3⁴ = 81.", + "log_b(b) = 1 a log_b(1) = 0.", + NULL, + }, + }, + { + "3 / 3 • Pravidla", "Součin a podíl", + "Tip: logaritmus součinu je součet logaritmů.", + { + "log_b(xy) = log_b(x) + log_b(y).", + "log_b(x/y) = log_b(x) − log_b(y).", + "log_b(xⁿ) = n · log_b(x).", + "Těmito pravidly se argument zjednodušuje, ne základ.", + NULL, + }, + }, + }; + + return math_unit_page(3, &mat3_lessons[1], "mat3_unit2", "mat3_unit2_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat3_unit2_exercise_page(void) { + static const MathItem qs[] = { + {"Spočítejte logaritmus o základu 10 z 1000.", "num:3", "10³ = 1000, takže logaritmus je 3. Výsledek je 3."}, + {"Spočítejte logaritmus o základu 2 z 32.", "num:5", "2⁵ = 32. Výsledek je 5."}, + {"Vyřešte 2ˣ = 16. Napište x.", "num:4", "16 = 2⁴, takže x = 4. Výsledek je 4."}, + {"Spočítejte logaritmus o základu 3 z 81.", "num:4", "3⁴ = 81. Výsledek je 4."}, + }; + + return math_practice_page(3, 1, "mat3unit2", + "mat3_ex2_title", "mat3_quiz2_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat3_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Jednotková kružnice", "Úhel od kladné poloosy x", + "Tip: sinus je y-ová souřadnice, kosinus x-ová.", + { + "Bod na jednotkové kružnici pod úhlem α má souřadnice [cos α; sin α].", + "cos 0° = 1 a sin 0° = 0.", + "sin 90° = 1 a cos 90° = 0.", + "cos 180° = −1 a sin 180° = 0.", + NULL, + }, + }, + { + "2 / 3 • 45°", "Polovina pravého úhlu", + "Tip: sin 45° = cos 45° = √2/2.", + { + "Úhel 45° půlí čtvrtkružnici.", + "Souřadnice bodu splňují x = y a x² + y² = 1.", + "Kladné řešení je √2/2.", + "Odpověď lze psát jako sqrt(2)/2 nebo 1/sqrt(2).", + NULL, + }, + }, + { + "3 / 3 • Znaménka", "Čtyři kvadranty", + "Tip: v prvním kvadrantu jsou sinus i kosinus kladné.", + { + "Ve druhém kvadrantu je sinus kladný a kosinus záporný.", + "Ve třetím jsou oba záporné.", + "Ve čtvrtém je kosinus kladný a sinus záporný.", + "Tangens je sinus dělený kosinem a není definován při cos α = 0.", + NULL, + }, + }, + }; + + return math_unit_page(3, &mat3_lessons[2], "mat3_unit3", "mat3_unit3_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat3_unit3_exercise_page(void) { + static const MathItem qs[] = { + {"Kolik je sin 90°?", "num:1", "Bod [0; 1] má y-ovou souřadnici 1. Výsledek je 1."}, + {"Kolik je cos 180°?", "num:-1", "Bod [−1; 0] má x-ovou souřadnici −1. Výsledek je -1."}, + {"Kolik je sin 45°? Můžete psát sqrt(2)/2.", "num:sqrt(2)/2", "sin 45° = √2/2. Stejnou hodnotu má 1/√2."}, + {"Kolik je cos 0°?", "num:1", "Úhel 0° je bod [1; 0]. Výsledek je 1."}, + }; + + return math_practice_page(3, 2, "mat3unit3", + "mat3_ex3_title", "mat3_quiz3_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat3_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Sinová věta", "a/sin α = 2R", + "Tip: poměr strany a sinu protilehlého úhlu je pro všechny strany stejný.", + { + "a/sin α = b/sin β = c/sin γ = 2R.", + "R je poloměr kružnice opsané.", + "Když a = 10 a α = 30°, je 10 / (1/2) = 20 = 2R.", + "Poloměr R je 10.", + NULL, + }, + }, + { + "2 / 3 • Kosinová věta", "c² = a² + b² − 2ab cos γ", + "Tip: pro pravý úhel je cos 90° = 0 a věta přejde v Pythagora.", + { + "Kosinová věta dopočítá stranu proti známému úhlu.", + "Jsou-li dvě strany 2 a sevřený úhel 60°, je třetí strana také 2.", + "a² = 4 + 4 − 2 · 2 · 2 · cos 60° = 8 − 8 · 1/2 = 4.", + "Rovnostranný trojúhelník má všechny úhly 60°.", + NULL, + }, + }, + { + "3 / 3 • Obsah", "Dvě strany a sevřený úhel", + "Tip: S = (1/2) · b · c · sin α.", + { + "Obsah je polovina součinu dvou stran a sinu sevřeného úhlu.", + "(1/2) · 4 · 6 · sin 30° = 12 · 1/2 = 6.", + "Sinová věta také dopočítá další stranu: b = a · sin β / sin α.", + "Pro a = 8, α = 30° a β = 45° je b = 8√2.", + NULL, + }, + }, + }; + + return math_unit_page(3, &mat3_lessons[3], "mat3_unit4", "mat3_unit4_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat3_unit4_exercise_page(void) { + static const MathItem qs[] = { + {"Strana a = 10 leží proti úhlu 30°. Jaký je poloměr kružnice opsané?", "num:10", "a/sin 30° = 20 = 2R, takže R = 10. Výsledek je 10."}, + {"Dvě strany trojúhelníku mají délku 2 a svírají úhel 60°. Jak dlouhá je třetí strana?", "num:2", "a² = 4 + 4 − 8 · 1/2 = 4, takže a = 2. Výsledek je 2."}, + {"Dvě strany mají délky 4 a 6 a svírají úhel 30°. Jaký je obsah trojúhelníku?", "num:6", "(1/2) · 4 · 6 · 1/2 = 6. Výsledek je 6."}, + {"Strana a = 8 je proti 30° a úhel β je 45°. Jak dlouhá je strana b? Sin 45° je √2/2.", "num:8*sqrt(2)", "b = 8 · sin 45° / sin 30° = 8 · (√2/2) / (1/2) = 8√2."}, + }; + + return math_practice_page(3, 3, "mat3unit4", + "mat3_ex4_title", "mat3_quiz4_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat3_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Aritmetická", "Stálý rozdíl", + "Tip: aₙ = a₁ + (n − 1)d.", + { + "Aritmetická posloupnost přičítá stále stejný rozdíl d.", + "Pro a₁ = 3 a d = 2 je a₅ = 3 + 4 · 2 = 11.", + "Součet prvních n členů je sₙ = n/2 · (2a₁ + (n − 1)d).", + "s₅ = 5/2 · (6 + 8) = 35.", + NULL, + }, + }, + { + "2 / 3 • Geometrická", "Stálý kvocient", + "Tip: aₙ = a₁ · qⁿ⁻¹.", + { + "Geometrická posloupnost násobí stále stejným kvocientem q.", + "Pro a₁ = 2 a q = 3 je a₄ = 2 · 3³ = 54.", + "q = 1 znamená, že se členy nemění.", + "q = 0 je po prvním členu samá nula, pokud je první člen definován.", + NULL, + }, + }, + { + "3 / 3 • Nekonečná řada", "Součet pro |q| < 1", + "Tip: s = a₁ / (1 − q).", + { + "Nekonečná geometrická řada má součet jen pro |q| < 1.", + "Pro a₁ = 6 a q = 1/2 je s = 6 / (1/2) = 12.", + "Pro |q| ≥ 1 se součet členů neblíží ke konečnému číslu, kromě případu q = 1 a a₁ = 0.", + "Řada 6 + 3 + 3/2 + … se tedy blíží ke 12.", + NULL, + }, + }, + }; + + return math_unit_page(3, &mat3_lessons[4], "mat3_unit5", "mat3_unit5_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat3_unit5_exercise_page(void) { + static const MathItem qs[] = { + {"Aritmetická posloupnost má a₁ = 3 a d = 2. Kolik je a₅?", "num:11", "a₅ = 3 + 4 · 2 = 11. Výsledek je 11."}, + {"Aritmetická posloupnost má a₁ = 3 a d = 2. Kolik je součet prvních pěti členů?", "num:35", "s₅ = 5/2 · (6 + 8) = 35. Výsledek je 35."}, + {"Geometrická posloupnost má a₁ = 2 a q = 3. Kolik je a₄?", "num:54", "a₄ = 2 · 27 = 54. Výsledek je 54."}, + {"Nekonečná geometrická řada má a₁ = 6 a q = 1/2. Jaký je její součet?", "num:12", "s = 6 / (1/2) = 12. Výsledek je 12."}, + }; + + return math_practice_page(3, 4, "mat3unit5", + "mat3_ex5_title", "mat3_quiz5_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat3_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Permutace", "Pořadí všech prvků", + "Tip: n! = 1 · 2 · … · n a 0! = 1.", + { + "Permutace je uspořádání všech n různých prvků.", + "Počet je n!.", + "4! = 24 a 3! = 6.", + "Na prvním místě je n možností, na dalším n − 1, a tak dál.", + NULL, + }, + }, + { + "2 / 3 • Variace", "Pořadí vybraných prvků", + "Tip: variace bez opakování V(n, k) = n! / (n − k)!.", + { + "Variace vybírá k prvků z n a záleží na pořadí.", + "V(5, 2) = 5 · 4 = 20.", + "Opakování by dovolilo vybrat stejný prvek víckrát.", + "Permutace je variace, která vybírá všech n prvků.", + NULL, + }, + }, + { + "3 / 3 • Kombinace", "Pořadí nehraje roli", + "Tip: C(n, k) = n! / (k! · (n − k)!).", + { + "Kombinace vybírá k prvků a dvě vybrané skupiny se stejným obsahem jsou jedna.", + "C(5, 2) = 10.", + "C(n, k) = C(n, n − k).", + "Počet variací je k! krát větší než počet kombinací.", + NULL, + }, + }, + }; + + return math_unit_page(3, &mat3_lessons[5], "mat3_unit6", "mat3_unit6_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat3_unit6_exercise_page(void) { + static const MathItem qs[] = { + {"Kolik je permutací čtyř různých prvků?", "num:24", "4! = 24. Výsledek je 24."}, + {"Kolik je kombinací 2 prvků z 5?", "num:10", "C(5, 2) = (5 · 4) / 2 = 10. Výsledek je 10."}, + {"Kolik je variací 2 prvků z 5 bez opakování?", "num:20", "V(5, 2) = 5 · 4 = 20. Výsledek je 20."}, + {"Kolik je 3!?", "num:6", "3 · 2 · 1 = 6. Výsledek je 6."}, + }; + + return math_practice_page(3, 5, "mat3unit6", + "mat3_ex6_title", "mat3_quiz6_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat3_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Definice", "Příznivé ku všem", + "Tip: všechny výsledky musí být stejně možné.", + { + "Klasická pravděpodobnost je počet příznivých výsledků dělený počtem všech.", + "Hodnota leží od 0 do 1.", + "Jev jistý má pravděpodobnost 1, jev nemožný 0.", + "Na kostce je sudé číslo ve třech případech ze šesti, tedy 1/2.", + NULL, + }, + }, + { + "2 / 3 • Nezávislost", "Součin pravděpodobností", + "Tip: dvě mince mají čtyři stejně možné dvojice výsledků.", + { + "U dvou nezávislých mincí jsou možnosti PP, PO, OP a OO.", + "Dva panny jsou jedna možnost ze čtyř, tedy 1/4.", + "U dvou kostek je 36 stejně možných dvojic.", + "Součet 7 nastane u šesti dvojic: 1+6, 2+5, 3+4, 4+3, 5+2 a 6+1.", + NULL, + }, + }, + { + "3 / 3 • Karty", "Čtyři esa v balíčku 32 karet", + "Tip: mariášový balíček má 32 karet.", + { + "Pravděpodobnost esa je 4/32 = 1/8.", + "Tažení jedné karty považuje každou kartu za stejně možnou.", + "Doplňkový jev má pravděpodobnost 1 − p.", + "Výsledek se krátí do základního tvaru.", + NULL, + }, + }, + }; + + return math_unit_page(3, &mat3_lessons[6], "mat3_unit7", "mat3_unit7_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat3_unit7_exercise_page(void) { + static const MathItem qs[] = { + {"Jaká je pravděpodobnost sudého čísla při hodu hrací kostkou?", "num:1/2", "Sudá jsou 2, 4 a 6, tedy 3 ze 6, čili 1/2. Výsledek je 1/2."}, + {"Jaká je pravděpodobnost, že na dvou mincích padnou dvě panny?", "num:1/4", "Ze čtyř dvojic je příznivá jedna. Výsledek je 1/4."}, + {"V balíčku 32 karet jsou 4 esa. Jaká je pravděpodobnost, že tažená karta je eso?", "num:1/8", "4/32 = 1/8. Výsledek je 1/8."}, + {"Jaká je pravděpodobnost, že součet na dvou hracích kostkách je 7?", "num:1/6", "Příznivých dvojic je 6 z 36, tedy 1/6. Výsledek je 1/6."}, + }; + + return math_practice_page(3, 6, "mat3unit7", + "mat3_ex7_title", "mat3_quiz7_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat3_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Krychle", "Objem a povrch", + "Tip: krychle o hraně a má objem a³ a povrch 6a².", + { + "Objem je počet jednotkových krychlí, které se do tělesa vejdou.", + "Krychle o hraně 3 má objem 27.", + "Povrch je součet obsahů všech stěn: 6 · 9 = 54.", + "Jednotky objemu jsou krychlové, jednotky povrchu čtvereční.", + NULL, + }, + }, + { + "2 / 3 • Válec a koule", "Vzorce s π", + "Tip: výsledek nechte v násobcích π.", + { + "Objem válce je πr²v.", + "Pro r = 2 a v = 5 je objem 20π.", + "Objem koule je (4/3)πr³.", + "Pro r = 3 je objem (4/3)π · 27 = 36π.", + NULL, + }, + }, + { + "3 / 3 • Jehlan", "Třetina hranolu", + "Tip: objem jehlanu je (1/3) · obsah podstavy · výška.", + { + "Jehlan má stejný objem jako třetina hranolu se stejnou podstavou a výškou.", + "Podstava o obsahu 12 a výška 4 dávají objem 16.", + "Výška je kolmá vzdálenost vrcholu od roviny podstavy.", + "Stejný vztah třetiny platí i pro kužel vůči válci.", + NULL, + }, + }, + }; + + return math_unit_page(3, &mat3_lessons[7], "mat3_unit8", "mat3_unit8_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat3_unit8_exercise_page(void) { + static const MathItem qs[] = { + {"Krychle má hranu 3. Jaký je její objem?", "num:27", "3³ = 27. Výsledek je 27."}, + {"Krychle má hranu 3. Jaký je její povrch?", "num:54", "Šest stěn o obsahu 9 dává 54. Výsledek je 54."}, + {"Válec má poloměr 2 a výšku 5. Jaký je jeho objem? Nechte π ve výsledku.", "num:20*pi", "πr²v = π · 4 · 5 = 20π. Výsledek je 20π."}, + {"Koule má poloměr 3. Jaký je její objem? Nechte π ve výsledku.", "num:36*pi", "(4/3)π · 27 = 36π. Výsledek je 36π."}, + }; + + return math_practice_page(3, 7, "mat3unit8", + "mat3_ex8_title", "mat3_quiz8_head", + qs, G_N_ELEMENTS(qs)); +} + +void add_mat3_pages(GtkStack *stack) { + typedef GtkWidget *(*MathBuilder)(void); + static const struct { + const char *unit_name; + const char *ex_name; + MathBuilder build_unit; + MathBuilder build_ex; + } pages[] = { + {"mat3unit1", "mat3ex1", build_mat3_unit1_page, build_mat3_unit1_exercise_page}, + {"mat3unit2", "mat3ex2", build_mat3_unit2_page, build_mat3_unit2_exercise_page}, + {"mat3unit3", "mat3ex3", build_mat3_unit3_page, build_mat3_unit3_exercise_page}, + {"mat3unit4", "mat3ex4", build_mat3_unit4_page, build_mat3_unit4_exercise_page}, + {"mat3unit5", "mat3ex5", build_mat3_unit5_page, build_mat3_unit5_exercise_page}, + {"mat3unit6", "mat3ex6", build_mat3_unit6_page, build_mat3_unit6_exercise_page}, + {"mat3unit7", "mat3ex7", build_mat3_unit7_page, build_mat3_unit7_exercise_page}, + {"mat3unit8", "mat3ex8", build_mat3_unit8_page, build_mat3_unit8_exercise_page}, + }; + + for (guint i = 0; i < G_N_ELEMENTS(pages); i++) { + gtk_stack_add_named(stack, pages[i].build_unit(), pages[i].unit_name); + gtk_stack_add_named(stack, pages[i].build_ex(), pages[i].ex_name); + } +} diff --git a/src/mat_year4.c b/src/mat_year4.c new file mode 100644 index 0000000..3e4ebea --- /dev/null +++ b/src/mat_year4.c @@ -0,0 +1,472 @@ +#include "graduately.h" + +NetLesson mat4_lessons[MATH_N] = { + { .n_slides = 3, .unit_page = "mat4unit1", .ex_page = "mat4ex1" }, + { .n_slides = 3, .unit_page = "mat4unit2", .ex_page = "mat4ex2" }, + { .n_slides = 3, .unit_page = "mat4unit3", .ex_page = "mat4ex3" }, + { .n_slides = 3, .unit_page = "mat4unit4", .ex_page = "mat4ex4" }, + { .n_slides = 3, .unit_page = "mat4unit5", .ex_page = "mat4ex5" }, + { .n_slides = 3, .unit_page = "mat4unit6", .ex_page = "mat4ex6" }, + { .n_slides = 3, .unit_page = "mat4unit7", .ex_page = "mat4ex7" }, + { .n_slides = 3, .unit_page = "mat4unit8", .ex_page = "mat4ex8" }, +}; + +static GtkWidget *build_mat4_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Souřadnice", "Velikost a součet", + "Tip: velikost vektoru (a; b) je √(a² + b²).", + { + "Vektor má velikost a směr.", + "|(3; 4)| = 5, protože 9 + 16 = 25.", + "Vektory se sčítají po souřadnicích.", + "(1; 2) + (3; −1) = (4; 1).", + NULL, + }, + }, + { + "2 / 3 • Skalární součin", "Číslo, ne vektor", + "Tip: (a; b) · (c; d) = ac + bd.", + { + "Skalární součin dvou vektorů je číslo.", + "(1; 2) · (3; 4) = 3 + 8 = 11.", + "Nulový skalární součin znamená kolmé vektory.", + "Velikost lze psát jako odmocninu ze skalárního součinu vektoru se sebou.", + NULL, + }, + }, + { + "3 / 3 • Vzdálenost", "Velikost rozdílu", + "Tip: na mřížce osa y míří nahoru.", + { + "Vzdálenost bodů je velikost vektoru mezi nimi.", + "Od [1; 1] do [4; 5] je vektor (3; 4) a vzdálenost 5.", + "Koncový bod vektoru (3; 2) umístěného do [1; 1] je [4; 3].", + "Pořadí souřadnic je x a potom y.", + NULL, + }, + }, + }; + + return math_unit_page(4, &mat4_lessons[0], "mat4_unit1", "mat4_unit1_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat4_unit1_exercise_page(void) { + static const MathItem qs[] = { + {"Jaká je velikost vektoru (3; 4)?", "num:5", "√(9 + 16) = √25 = 5. Výsledek je 5."}, + {"Sečtěte vektory (1; 2) a (3; −1). Napište souřadnice oddělené středníkem.", "pair:4;1", "(1 + 3; 2 + (−1)) = (4; 1). Napište 4;1."}, + {"Spočítejte skalární součin vektorů (1; 2) a (3; 4).", "num:11", "1 · 3 + 2 · 4 = 11. Výsledek je 11."}, + {"Jaká je vzdálenost bodů [1; 1] a [4; 5]?", "num:5", "Rozdíl je (3; 4) a jeho velikost je 5. Výsledek je 5."}, + {"Z bodu A = [1; 1] naneste vektor (3; 2). Zakreslete koncový bod.", "points:4,3;fix:1,1", "Ke každé souřadnici se přičte složka vektoru: [1 + 3; 1 + 2] = [4; 3]."}, + }; + + return math_practice_page(4, 0, "mat4unit1", + "mat4_ex1_title", "mat4_quiz1_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat4_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Směrnice", "y = ax + b", + "Tip: a = (y₂ − y₁)/(x₂ − x₁).", + { + "Body [1; 2] a [4; 8] určují směrnici (8 − 2)/(4 − 1) = 2.", + "Přímka procházející bodem [0; 1] se směrnicí 2 má rovnici y = 2x + 1.", + "Úsek na ose y je 1.", + "Svislá přímka směrnici tohoto tvaru nemá.", + NULL, + }, + }, + { + "2 / 3 • Parametr", "Bod a směrový vektor", + "Tip: X = A + t · u.", + { + "Parametrické vyjádření je x = x₀ + t · u₁, y = y₀ + t · u₂.", + "Přímka (1; 3) + t(2; 0) má při t = 2 bod [5; 3].", + "Každé t dá právě jeden bod přímky.", + "Směrový vektor (2; 0) je vodorovný.", + NULL, + }, + }, + { + "3 / 3 • Vzdálenost", "Bod od přímky", + "Tip: pro ax + by + c = 0 je vzdálenost |ax₀ + by₀ + c| / √(a² + b²).", + { + "Přímka 3x + 4y − 10 = 0 má a = 3, b = 4 a c = −10.", + "Vzdálenost počátku je |−10| / √(9 + 16) = 10/5 = 2.", + "Čitatel je absolutní hodnota, vzdálenost není záporná.", + "Čtvrtý vrchol rovnoběžníku k bodům A, B a C je B + C − A.", + NULL, + }, + }, + }; + + return math_unit_page(4, &mat4_lessons[1], "mat4_unit2", "mat4_unit2_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat4_unit2_exercise_page(void) { + static const MathItem qs[] = { + {"Jaká je směrnice přímky body [1; 2] a [4; 8]?", "num:2", "(8 − 2)/(4 − 1) = 2. Výsledek je 2."}, + {"Přímka má rovnici y = 2x + 1. Jaký je její úsek na ose y?", "num:1", "Úsek je absolutní člen, tedy 1. Výsledek je 1."}, + {"Bod přímky je (1; 3) + t(2; 0). Kam přímka dojde pro t = 2? Napište x a y středníkem.", "pair:5;3", "(1; 3) + 2 · (2; 0) = (5; 3). Napište 5;3."}, + {"Jaká je vzdálenost bodu [0; 0] od přímky 3x + 4y − 10 = 0?", "num:2", "|−10| / √(9 + 16) = 10/5 = 2. Výsledek je 2."}, + {"A = [1; 1], B = [4; 1] a C = [2; 3]. Zakreslete čtvrtý vrchol rovnoběžníku, ve kterém jsou AB a AC sousední strany.", "para:1,1;4,1;2,3", "D = B + C − A = [4 + 2 − 1; 1 + 3 − 1] = [5; 3]."}, + }; + + return math_practice_page(4, 1, "mat4unit2", + "mat4_ex2_title", "mat4_quiz2_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat4_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Kružnice", "Střed a poloměr", + "Tip: (x − m)² + (y − n)² = r² má střed [m; n].", + { + "Kružnice je množina bodů stejně vzdálených od středu.", + "(x − 1)² + (y + 2)² = 9 má střed [1; −2].", + "y + 2 je totéž co y − (−2).", + "Poloměr je √9 = 3, ne 9.", + NULL, + }, + }, + { + "2 / 3 • Elipsa", "Dvě poloosy", + "Tip: ve tvaru x²/a² + y²/b² = 1 je a poloosa na ose x.", + { + "Elipsa v osové poloze se středem v počátku má rovnici x²/a² + y²/b² = 1.", + "U x²/25 + y²/9 = 1 je a = 5 a b = 3.", + "Delší poloosa je hlavní, kratší vedlejší.", + "Vrcholy na ose x jsou [±a; 0], na ose y [0; ±b].", + NULL, + }, + }, + { + "3 / 3 • Excentricita", "Vzdálenost ohniska od středu", + "Tip: pro a > b je e = √(a² − b²).", + { + "Lineární výstřednost e splňuje e² = a² − b², když je a hlavní poloosa.", + "√(25 − 9) = √16 = 4.", + "Ohniska elipsy jsou [±e; 0].", + "Součet vzdáleností bodu elipsy od obou ohnisek je 2a.", + NULL, + }, + }, + }; + + return math_unit_page(4, &mat4_lessons[2], "mat4_unit3", "mat4_unit3_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat4_unit3_exercise_page(void) { + static const MathItem qs[] = { + {"Kružnice má rovnici (x − 1)² + (y + 2)² = 9. Napište souřadnice středu jako x;y.", "pair:1;-2", "Střed je [1; −2]. Napište 1;-2."}, + {"Jaký je poloměr kružnice (x − 1)² + (y + 2)² = 9?", "num:3", "Pravá strana je r², takže r = 3. Výsledek je 3."}, + {"Elipsa má rovnici x²/25 + y²/9 = 1. Jak dlouhá je poloosa a na ose x?", "num:5", "a² = 25, takže a = 5. Výsledek je 5."}, + {"Elipsa má rovnici x²/25 + y²/9 = 1. Jak dlouhá je poloosa b na ose y?", "num:3", "b² = 9, takže b = 3. Výsledek je 3."}, + {"Elipsa má a = 5 a b = 3. Jaká je její lineární výstřednost e?", "num:4", "e = √(25 − 9) = √16 = 4. Výsledek je 4."}, + }; + + return math_practice_page(4, 2, "mat4unit3", + "mat4_ex3_title", "mat4_quiz3_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat4_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Tvar", "a + bi", + "Tip: i² = −1. Reálnou a imaginární část pište oddělené středníkem.", + { + "Komplexní číslo a + bi má reálnou část a a imaginární část b.", + "i je imaginární jednotka a i² = −1.", + "Čísla se sčítají po složkách.", + "(2 + 3i) + (1 − i) = 3 + 2i.", + NULL, + }, + }, + { + "2 / 3 • Násobení", "Jako dvojčlen", + "Tip: (1 + i)(1 − i) je rozdíl čtverců.", + { + "(a + bi)(c + di) = ac + adi + bci + bdi².", + "bdi² = −bd, proto se imaginární jednotka ve výsledku nemusí objevit.", + "(1 + i)(1 − i) = 1 − (i)² = 1 − (−1) = 2.", + "Komplexní čísla lze násobit i v goniometrickém tvaru sčítáním argumentů.", + NULL, + }, + }, + { + "3 / 3 • Absolutní hodnota", "Vzdálenost od nuly", + "Tip: |a + bi| = √(a² + b²).", + { + "Absolutní hodnota je vzdálenost obrazu čísla od počátku Gaussovy roviny.", + "|3 + 4i| = 5.", + "|z| je vždy reálné nezáporné číslo.", + "i² = −1, i³ = −i a i⁴ = 1.", + NULL, + }, + }, + }; + + return math_unit_page(4, &mat4_lessons[3], "mat4_unit4", "mat4_unit4_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat4_unit4_exercise_page(void) { + static const MathItem qs[] = { + {"Sečtěte 2 + 3i a 1 − i. Napište reálnou a imaginární část středníkem.", "pair:3;2", "(2 + 1) + (3 − 1)i = 3 + 2i. Napište 3;2."}, + {"Spočítejte (1 + i)(1 − i).", "num:2", "1 − i² = 1 − (−1) = 2. Výsledek je 2."}, + {"Jaká je absolutní hodnota čísla 3 + 4i?", "num:5", "√(9 + 16) = 5. Výsledek je 5."}, + {"Kolik je i²?", "num:-1", "Imaginární jednotka splňuje i² = −1. Výsledek je -1."}, + }; + + return math_practice_page(4, 3, "mat4unit4", + "mat4_ex4_title", "mat4_quiz4_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat4_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Mocnina", "(xⁿ)' = n xⁿ⁻¹", + "Tip: derivace je směrnice tečny.", + { + "Derivace funkce v bodě je směrnice tečny grafu v tom bodě.", + "(x³)' = 3x². V bodě x = 2 je 3 · 4 = 12.", + "(5x²)' = 10x. V bodě x = 1 je 10.", + "Derivace konstanty je 0 a konstanta se vytýká.", + NULL, + }, + }, + { + "2 / 3 • Tečna", "Směrnice v bodě", + "Tip: u f(x) = x² je f'(x) = 2x.", + { + "Tečna ke grafu y = x² v bodě x = 1 má směrnici 2.", + "Rovnice tečny používá bod [1; 1] a tuto směrnici.", + "Kladná derivace znamená, že funkce v bodě roste.", + "Záporná derivace znamená, že funkce klesá.", + NULL, + }, + }, + { + "3 / 3 • Sinus", "Derivace sinu je kosinus", + "Tip: (sin x)' = cos x a cos 0 = 1.", + { + "Derivace funkce sin x je cos x.", + "Derivace funkce cos x je −sin x.", + "V bodě x = 0 je derivace sinu rovna cos 0 = 1.", + "Úhel v tomto vzorci je v radiánech, ale nula je nula v obou jednotkách.", + NULL, + }, + }, + }; + + return math_unit_page(4, &mat4_lessons[4], "mat4_unit5", "mat4_unit5_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat4_unit5_exercise_page(void) { + static const MathItem qs[] = { + {"Jaká je derivace funkce x³ v bodě x = 2?", "num:12", "(x³)' = 3x² a 3 · 4 = 12. Výsledek je 12."}, + {"Jaká je derivace funkce 5x² v bodě x = 1?", "num:10", "(5x²)' = 10x a v jedničce je 10. Výsledek je 10."}, + {"Jakou směrnici má tečna ke grafu y = x² v bodě x = 1?", "num:2", "Derivace 2x je v bodě 1 rovna 2. Výsledek je 2."}, + {"Jaká je derivace funkce sin x v bodě x = 0? Derivace sinu je kosinus.", "num:1", "(sin x)' = cos x a cos 0 = 1. Výsledek je 1."}, + }; + + return math_practice_page(4, 4, "mat4unit5", + "mat4_ex5_title", "mat4_quiz5_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat4_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Stacionární bod", "Derivace je nula", + "Tip: f'(x) = 0 je vodorovná tečna.", + { + "V lokálním extrému diferencovatelné funkce je derivace nulová.", + "U f(x) = x³ − 3x je f'(x) = 3x² − 3.", + "3(x² − 1) = 0 dává x = −1 a x = 1.", + "To jsou jediní kandidáti na lokální extrém.", + NULL, + }, + }, + { + "2 / 3 • Maximum a minimum", "Znaménko druhé derivace", + "Tip: f''(x) = 6x. Záporná druhá derivace znamená lokální maximum.", + { + "V x = −1 je f''(−1) = −6 < 0, takže jde o lokální maximum.", + "f(−1) = −1 + 3 = 2.", + "V x = 1 je f''(1) = 6 > 0, takže jde o lokální minimum.", + "f(1) = 1 − 3 = −2.", + NULL, + }, + }, + { + "3 / 3 • Jiný příklad", "Lineární derivace", + "Tip: g'(x) = 2x − 4 je nula při x = 2.", + { + "Kde je derivace nula, má graf vodorovnou tečnu.", + "Rovnice 2x − 4 = 0 má řešení x = 2.", + "Pro x < 2 je derivace 2x − 4 záporná, funkce klesá.", + "Pro x > 2 je derivace kladná, funkce roste.", + NULL, + }, + }, + }; + + return math_unit_page(4, &mat4_lessons[5], "mat4_unit6", "mat4_unit6_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat4_unit6_exercise_page(void) { + static const MathItem qs[] = { + {"Funkce f(x) = x³ − 3x. Ve kterém x má lokální maximum?", "num:-1", "f'(x) = 3x² − 3 = 0 pro x = ±1 a v −1 je maximum. Výsledek je -1."}, + {"Funkce f(x) = x³ − 3x. Jaká je hodnota lokálního maxima?", "num:2", "f(−1) = −1 + 3 = 2. Výsledek je 2."}, + {"Funkce f(x) = x³ − 3x. Ve kterém x má lokální minimum?", "num:1", "Druhá derivace 6x je v x = 1 kladná. Výsledek je 1."}, + {"Derivace funkce je g'(x) = 2x − 4. Pro které x je derivace nulová?", "num:2", "2x − 4 = 0 dává x = 2. Výsledek je 2."}, + }; + + return math_practice_page(4, 5, "mat4unit6", + "mat4_ex6_title", "mat4_quiz6_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat4_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Primitivní funkce", "Opačný postup k derivaci", + "Tip: primitivní funkce k xⁿ je xⁿ⁺¹/(n + 1) pro n ≠ −1.", + { + "Neurčitý integrál je množina primitivních funkcí.", + "Liší se o konstantu, protože derivace konstanty je nula.", + "Primitivní funkce k 2x je x².", + "Primitivní funkce k x² je x³/3.", + NULL, + }, + }, + { + "2 / 3 • Určitý integrál", "Newtonův vzorec", + "Tip: ∫ od a do b z f je F(b) − F(a).", + { + "Určitý integrál nezáporné funkce je obsah plochy pod grafem.", + "∫ od 0 do 3 z 2x dx = [x²] od 0 do 3 = 9.", + "∫ od 0 do 2 z x² dx = [x³/3] od 0 do 2 = 8/3.", + "Dolní mez 0 u těchto mocnin často vynuluje první člen.", + NULL, + }, + }, + { + "3 / 3 • Konstanta", "Násobek lze vytknout", + "Tip: integrál z c · f je c krát integrál z f.", + { + "∫ od 0 do 1 z 3x² dx = [x³] od 0 do 1 = 1.", + "∫ od 0 do 2 z 6x dx = [3x²] od 0 do 2 = 12.", + "Záporná funkce dává záporný integrál, nejde vždy o obsah beze znaménka.", + "Součet funkcí se integruje člen po členu.", + NULL, + }, + }, + }; + + return math_unit_page(4, &mat4_lessons[6], "mat4_unit7", "mat4_unit7_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat4_unit7_exercise_page(void) { + static const MathItem qs[] = { + {"Spočítejte integrál od 0 do 3 z funkce 2x.", "num:9", "Primitivní funkce je x² a 9 − 0 = 9. Výsledek je 9."}, + {"Spočítejte integrál od 0 do 2 z funkce x².", "num:8/3", "Primitivní funkce je x³/3 a 8/3 − 0 = 8/3. Výsledek je 8/3."}, + {"Spočítejte integrál od 0 do 1 z funkce 3x².", "num:1", "Primitivní funkce je x³ a 1 − 0 = 1. Výsledek je 1."}, + {"Spočítejte integrál od 0 do 2 z funkce 6x.", "num:12", "Primitivní funkce je 3x² a 12 − 0 = 12. Výsledek je 12."}, + }; + + return math_practice_page(4, 6, "mat4unit7", + "mat4_ex7_title", "mat4_quiz7_head", + qs, G_N_ELEMENTS(qs)); +} + +static GtkWidget *build_mat4_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Číslo", "Logaritmus a zlomek", + "Tip: log₂(8) = 3, protože 2³ = 8.", + { + "Logaritmus vrací exponent.", + "1/2 + 1/3 = 5/6.", + "Společný jmenovatel se hledá i v maturitním příkladu stejně jako na základní škole.", + "Výsledek se krátí.", + NULL, + }, + }, + { + "2 / 3 • Počet a vektor", "Kombinace a velikost", + "Tip: C(6, 2) = 15 a |(3; 4)| = 5.", + { + "C(6, 2) = (6 · 5) / 2 = 15.", + "Pořadí vybrané dvojice se nepočítá dvakrát.", + "Velikost vektoru (3; 4) je přepona trojúhelníku 3-4-5.", + "Skalární součin by u kolmých vektorů vyšel nula.", + NULL, + }, + }, + { + "3 / 3 • Rovnice", "x² − 1 = 0", + "Tip: rozdíl čtverců (x − 1)(x + 1).", + { + "x² − 1 = 0 má kořeny −1 a 1.", + "Oba se napíšou, oddělené středníkem.", + "Diskriminant je 0 − 4 · 1 · (−1) = 4.", + "Zkouška: 1 − 1 = 0 a 1 − 1 = 0.", + NULL, + }, + }, + }; + + return math_unit_page(4, &mat4_lessons[7], "mat4_unit8", "mat4_unit8_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_mat4_unit8_exercise_page(void) { + static const MathItem qs[] = { + {"Spočítejte logaritmus o základu 2 z 8.", "num:3", "2³ = 8. Výsledek je 3."}, + {"Kolik je kombinací 2 prvků ze 6?", "num:15", "C(6, 2) = 15. Výsledek je 15."}, + {"Jaká je velikost vektoru (3; 4)?", "num:5", "√(9 + 16) = 5. Výsledek je 5."}, + {"Spočítejte 1/2 + 1/3.", "num:5/6", "3/6 + 2/6 = 5/6. Výsledek je 5/6."}, + {"Najděte kořeny rovnice x² − 1 = 0. Oddělte je středníkem.", "set:-1;1", "(x − 1)(x + 1) = 0. Napište -1;1."}, + }; + + return math_practice_page(4, 7, "mat4unit8", + "mat4_ex8_title", "mat4_quiz8_head", + qs, G_N_ELEMENTS(qs)); +} + +void add_mat4_pages(GtkStack *stack) { + typedef GtkWidget *(*MathBuilder)(void); + static const struct { + const char *unit_name; + const char *ex_name; + MathBuilder build_unit; + MathBuilder build_ex; + } pages[] = { + {"mat4unit1", "mat4ex1", build_mat4_unit1_page, build_mat4_unit1_exercise_page}, + {"mat4unit2", "mat4ex2", build_mat4_unit2_page, build_mat4_unit2_exercise_page}, + {"mat4unit3", "mat4ex3", build_mat4_unit3_page, build_mat4_unit3_exercise_page}, + {"mat4unit4", "mat4ex4", build_mat4_unit4_page, build_mat4_unit4_exercise_page}, + {"mat4unit5", "mat4ex5", build_mat4_unit5_page, build_mat4_unit5_exercise_page}, + {"mat4unit6", "mat4ex6", build_mat4_unit6_page, build_mat4_unit6_exercise_page}, + {"mat4unit7", "mat4ex7", build_mat4_unit7_page, build_mat4_unit7_exercise_page}, + {"mat4unit8", "mat4ex8", build_mat4_unit8_page, build_mat4_unit8_exercise_page}, + }; + + for (guint i = 0; i < G_N_ELEMENTS(pages); i++) { + gtk_stack_add_named(stack, pages[i].build_unit(), pages[i].unit_name); + gtk_stack_add_named(stack, pages[i].build_ex(), pages[i].ex_name); + } +} diff --git a/src/matematika.c b/src/matematika.c new file mode 100644 index 0000000..0eb6831 --- /dev/null +++ b/src/matematika.c @@ -0,0 +1,955 @@ +#include "graduately.h" + +#include + +/* Mathematics: elementary review plus four gymnasium years. + * Lessons are slides, then typed examples. A few lessons also + * ask for a point snapped to the grid. */ + +#define MATH_CELL 28.0 +#define MATH_PAD 18.0 +#define MATH_GX 10 +#define MATH_GY 8 +#define MATH_PTS 16 + +typedef struct { + const char *map_page; + const char *back_page; + const char *title_key; + const char *sub_key; + char **progress_path; + NetLesson *lessons; + const char *unit_keys[MATH_N]; + const char *sub_keys[MATH_N]; + GtkWidget *nodes[MATH_N]; + int note_last_w; +} MathTrack; + +typedef struct { + const char *spec; + int ux[MATH_PTS]; + int uy[MATH_PTS]; + int n; + int fx[8]; + int fy[8]; + int nf; + int need; + int checked; + int correct; + GtkWidget *area; +} MathBoard; + +typedef struct { + const MathItem *items; + int n; + MathTrack *track; + int lesson_id; + GtkWidget *feedback; + GtkWidget **entries; + MathBoard **boards; + GtkWidget **hints; +} MathExCtx; + +static MathTrack math_tracks[5]; +static gboolean math_ready; +static char math_key_store[5][MATH_N][32]; +static char math_sub_store[5][MATH_N][40]; + +static void math_ensure(void) { + static const char *prefixes[] = {"mat0", "mat", "mat2", "mat3", "mat4"}; + static const char *maps[] = { + "mat0map", "matmap", "mat2map", "mat3map", "mat4map", + }; + static const char *titles[] = { + "mat0_year", "mat_year1", "mat_year2", "mat_year3", "mat_year4", + }; + static const char *subs[] = { + "mat0_sub", "mat_sub", "mat2_sub", "mat3_sub", "mat4_sub", + }; + NetLesson *lessons[5] = { + mat0_lessons, mat_lessons, mat2_lessons, mat3_lessons, mat4_lessons, + }; + char **paths[5] = { + &app_progress_mat0, &app_progress_mat, &app_progress_mat2, + &app_progress_mat3, &app_progress_mat4, + }; + int y, i; + + if (math_ready) + return; + for (y = 0; y < 5; y++) { + MathTrack *T = &math_tracks[y]; + + T->map_page = maps[y]; + T->back_page = "matyears"; + T->title_key = titles[y]; + T->sub_key = subs[y]; + T->progress_path = paths[y]; + T->lessons = lessons[y]; + T->note_last_w = -1; + for (i = 0; i < MATH_N; i++) { + g_snprintf(math_key_store[y][i], sizeof math_key_store[y][i], + "%s_unit%d", prefixes[y], i + 1); + g_snprintf(math_sub_store[y][i], sizeof math_sub_store[y][i], + "%s_unit%d_sub", prefixes[y], i + 1); + T->unit_keys[i] = math_key_store[y][i]; + T->sub_keys[i] = math_sub_store[y][i]; + } + } + math_ready = TRUE; +} + +static void math_save(MathTrack *T) { + GKeyFile *kf; + gchar *data; + + if (!T->progress_path || !*T->progress_path) + return; + kf = g_key_file_new(); + for (int i = 0; i < MATH_N; i++) { + gchar key[8]; + + g_snprintf(key, sizeof key, "%d", i + 1); + g_key_file_set_boolean(kf, "done", key, T->lessons[i].done); + } + if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) { + g_key_file_free(kf); + return; + } + data = g_key_file_to_data(kf, NULL, NULL); + g_key_file_free(kf); + if (data) { + GError *err = NULL; + + g_file_set_contents(*T->progress_path, data, -1, &err); + if (err) + g_error_free(err); + g_free(data); + } +} + +static void math_load_one(MathTrack *T) { + GKeyFile *kf; + GError *err = NULL; + + if (!T->progress_path || !*T->progress_path) + return; + kf = g_key_file_new(); + if (!g_key_file_load_from_file(kf, *T->progress_path, G_KEY_FILE_NONE, &err)) { + if (err) + g_error_free(err); + g_key_file_free(kf); + return; + } + for (int i = 0; i < MATH_N; i++) { + gchar key[8]; + + g_snprintf(key, sizeof key, "%d", i + 1); + T->lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL); + } + g_key_file_free(kf); +} + +void math_load_progress(void) { + math_ensure(); + for (int y = 0; y < 5; y++) + math_load_one(&math_tracks[y]); +} + +void math_rail_theme_reset(void) { +} + +static void math_refresh(MathTrack *T) { + for (int i = 0; i < MATH_N; i++) { + GtkWidget *node = T->nodes[i]; + GtkWidget *icon = T->lessons[i].done_icon; + + if (!node) + continue; + if (T->lessons[i].done) { + gtk_widget_remove_css_class(node, "current"); + gtk_widget_add_css_class(node, "done"); + if (icon) + gtk_widget_set_visible(icon, TRUE); + } else { + gtk_widget_remove_css_class(node, "done"); + gtk_widget_add_css_class(node, "current"); + if (icon) + gtk_widget_set_visible(icon, FALSE); + } + } +} + +static void math_mark_done(MathTrack *T, int lesson_id) { + if (lesson_id < 0 || lesson_id >= MATH_N) + return; + if (T->lessons[lesson_id].done) + return; + T->lessons[lesson_id].done = TRUE; + math_save(T); + math_refresh(T); + refresh_stats_ui(); +} + +void progress_for_math(ProgressSum *out) { + if (!out) + return; + math_ensure(); + for (int y = 0; y < 5; y++) { + MathTrack *T = &math_tracks[y]; + + out->total_ex += MATH_N; + out->open_units += MATH_N; + for (int i = 0; i < MATH_N; i++) { + if (T->lessons[i].done) { + out->done_ex++; + out->done_units++; + } + } + } +} + +static void math_open_unit(GtkButton *button, gpointer data) { + NetLesson *L = data; + + (void)button; + if (!L) + return; + L->idx = 0; + if (L->stack) { + char name[16]; + + g_snprintf(name, sizeof name, "s%u", L->idx); + gtk_stack_set_visible_child_name(GTK_STACK(L->stack), name); + if (L->prev_btn) + gtk_widget_set_sensitive(L->prev_btn, FALSE); + if (L->next_btn) + gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next")); + } + gtk_stack_set_visible_child_name(main_stack, L->unit_page); +} + +static GtkWidget *math_lesson_button(MathTrack *T, int index) { + GtkWidget *btn; + GtkWidget *row; + GtkWidget *num; + GtkWidget *texts; + GtkWidget *title; + GtkWidget *sub; + GtkWidget *icon; + char *num_text; + + btn = gtk_button_new(); + gtk_widget_add_css_class(btn, "unit-node"); + gtk_widget_add_css_class(btn, "current"); + gtk_widget_set_can_focus(btn, FALSE); + gtk_widget_set_hexpand(btn, TRUE); + gtk_widget_set_size_request(btn, -1, 84); + + row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14); + gtk_widget_set_halign(row, GTK_ALIGN_START); + gtk_widget_set_valign(row, GTK_ALIGN_CENTER); + gtk_widget_set_margin_start(row, 18); + gtk_widget_set_margin_end(row, 18); + gtk_button_set_child(GTK_BUTTON(btn), row); + + num_text = g_strdup_printf("%d", index + 1); + num = gtk_label_new(num_text); + g_free(num_text); + gtk_widget_add_css_class(num, "unit-number"); + gtk_widget_set_valign(num, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(row), num); + + texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + gtk_widget_set_halign(texts, GTK_ALIGN_START); + gtk_widget_set_hexpand(texts, TRUE); + gtk_box_append(GTK_BOX(row), texts); + + title = gtk_label_new(NULL); + i18n_bind(title, T->unit_keys[index], 0); + gtk_widget_set_halign(title, GTK_ALIGN_START); + gtk_widget_add_css_class(title, "unit-name"); + gtk_box_append(GTK_BOX(texts), title); + + sub = gtk_label_new(NULL); + i18n_bind(sub, T->sub_keys[index], 0); + gtk_widget_set_halign(sub, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(sub), TRUE); + gtk_label_set_xalign(GTK_LABEL(sub), 0.0); + gtk_widget_add_css_class(sub, "hint"); + gtk_box_append(GTK_BOX(texts), sub); + + icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 18); + gtk_widget_set_visible(icon, FALSE); + gtk_widget_set_valign(icon, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(row), icon); + T->lessons[index].done_icon = icon; + T->nodes[index] = btn; + g_signal_connect(btn, "clicked", G_CALLBACK(math_open_unit), + &T->lessons[index]); + return btn; +} + +static GtkWidget *math_build_list(MathTrack *T) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *list; + + math_ensure(); + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_hexpand(page, TRUE); + gtk_widget_set_vexpand(page, TRUE); + gtk_widget_set_margin_start(page, 32); + gtk_widget_set_margin_end(page, 32); + gtk_widget_set_margin_top(page, 24); + gtk_widget_set_margin_bottom(page, 24); + + gtk_box_append(GTK_BOX(page), + top_bar(T->back_page, T->title_key, T->sub_key)); + + scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_margin_top(scroll, 18); + gtk_box_append(GTK_BOX(page), scroll); + + list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_set_halign(list, GTK_ALIGN_CENTER); + gtk_widget_set_valign(list, GTK_ALIGN_START); + gtk_widget_set_size_request(list, 520, -1); + gtk_widget_set_margin_bottom(list, 12); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list); + + for (int i = 0; i < MATH_N; i++) + gtk_box_append(GTK_BOX(list), math_lesson_button(T, i)); + math_refresh(T); + return page; +} + +GtkWidget *build_mat0map_page(void) { return math_build_list(&math_tracks[0]); } +GtkWidget *build_matmap_page(void) { return math_build_list(&math_tracks[1]); } +GtkWidget *build_mat2map_page(void) { return math_build_list(&math_tracks[2]); } +GtkWidget *build_mat3map_page(void) { return math_build_list(&math_tracks[3]); } +GtkWidget *build_mat4map_page(void) { return math_build_list(&math_tracks[4]); } + +static GtkWidget *math_year_button(const char *badge, const char *target, + const char *title_key, const char *sub_key) { + GtkWidget *btn; + GtkWidget *row; + GtkWidget *num; + GtkWidget *texts; + GtkWidget *title; + GtkWidget *sub; + + btn = gtk_button_new(); + gtk_widget_add_css_class(btn, "unit-node"); + gtk_widget_add_css_class(btn, "current"); + gtk_widget_set_can_focus(btn, FALSE); + gtk_widget_set_hexpand(btn, TRUE); + gtk_widget_set_size_request(btn, -1, 84); + + row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14); + gtk_widget_set_halign(row, GTK_ALIGN_START); + gtk_widget_set_valign(row, GTK_ALIGN_CENTER); + gtk_widget_set_margin_start(row, 18); + gtk_widget_set_margin_end(row, 18); + gtk_button_set_child(GTK_BUTTON(btn), row); + + num = gtk_label_new(badge); + gtk_widget_add_css_class(num, "unit-number"); + gtk_widget_set_valign(num, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(row), num); + + texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + gtk_widget_set_halign(texts, GTK_ALIGN_START); + gtk_widget_set_hexpand(texts, TRUE); + gtk_box_append(GTK_BOX(row), texts); + + title = gtk_label_new(NULL); + i18n_bind(title, title_key, 0); + gtk_widget_set_halign(title, GTK_ALIGN_START); + gtk_widget_add_css_class(title, "unit-name"); + gtk_box_append(GTK_BOX(texts), title); + + sub = gtk_label_new(NULL); + i18n_bind(sub, sub_key, 0); + gtk_widget_set_halign(sub, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(sub), TRUE); + gtk_label_set_xalign(GTK_LABEL(sub), 0.0); + gtk_widget_add_css_class(sub, "hint"); + gtk_box_append(GTK_BOX(texts), sub); + + g_object_set_data_full(G_OBJECT(btn), "target", g_strdup(target), g_free); + g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL); + return btn; +} + +GtkWidget *build_matyears_page(void) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *list; + static const char *badges[] = {"ZŠ", "1.", "2.", "3.", "4."}; + static const char *targets[] = { + "mat0map", "matmap", "mat2map", "mat3map", "mat4map", + }; + static const char *titles[] = { + "mat0_year", "mat_year1", "mat_year2", "mat_year3", "mat_year4", + }; + static const char *subs[] = { + "mat0_sub", "mat_sub", "mat2_sub", "mat3_sub", "mat4_sub", + }; + + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_hexpand(page, TRUE); + gtk_widget_set_vexpand(page, TRUE); + gtk_widget_set_margin_start(page, 32); + gtk_widget_set_margin_end(page, 32); + gtk_widget_set_margin_top(page, 24); + gtk_widget_set_margin_bottom(page, 24); + + gtk_box_append(GTK_BOX(page), + top_bar("subjects", "Matematika", "mat_years_sub")); + + scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_margin_top(scroll, 18); + gtk_box_append(GTK_BOX(page), scroll); + + list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_set_halign(list, GTK_ALIGN_CENTER); + gtk_widget_set_valign(list, GTK_ALIGN_CENTER); + gtk_widget_set_size_request(list, 480, -1); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list); + + for (int y = 0; y < 5; y++) + gtk_box_append(GTK_BOX(list), + math_year_button(badges[y], targets[y], titles[y], subs[y])); + return page; +} + +static void math_slide_apply(NetLesson *L) { + char name[16]; + + if (!L || !L->stack) + return; + g_snprintf(name, sizeof name, "s%u", L->idx); + gtk_stack_set_visible_child_name(GTK_STACK(L->stack), name); + if (L->prev_btn) { + gtk_button_set_label(GTK_BUTTON(L->prev_btn), tr("net_slide_prev")); + gtk_widget_set_sensitive(L->prev_btn, L->idx > 0); + } + if (L->next_btn) { + gtk_button_set_label( + GTK_BUTTON(L->next_btn), + L->idx + 1 >= L->n_slides ? tr("net_slide_start") + : tr("net_slide_next")); + } +} + +static void math_slide_prev(GtkButton *button, gpointer data) { + NetLesson *L = data; + + (void)button; + if (!L || L->idx == 0) + return; + L->idx--; + math_slide_apply(L); +} + +static void math_slide_next(GtkButton *button, gpointer data) { + NetLesson *L = data; + + (void)button; + if (!L) + return; + if (L->idx + 1 < L->n_slides) { + L->idx++; + math_slide_apply(L); + } else { + gtk_stack_set_visible_child_name(main_stack, L->ex_page); + } +} + +static void math_rescale(MathTrack *T, int w) { + int body, head, kick; + + if (w < 160) + w = 160; + body = w / 46; + if (body < 14) + body = 14; + if (body > 26) + body = 26; + head = body + 14; + if (head > 46) + head = 46; + kick = body - 3; + if (kick < 11) + kick = 11; + for (int i = 0; i < MATH_N; i++) + net_rescale_lesson_notes(&T->lessons[i], body, head, kick); +} + +static gboolean math_note_tick(GtkWidget *w, GdkFrameClock *clock, gpointer data) { + MathTrack *T = data; + int width = gtk_widget_get_allocated_width(w); + + (void)clock; + if (width != T->note_last_w) { + T->note_last_w = width; + math_rescale(T, width); + } + return G_SOURCE_CONTINUE; +} + +GtkWidget *math_unit_page(int year, NetLesson *L, const char *title_key, + const char *sub_key, const NetSlide *slides, + guint n_slides) { + MathTrack *T; + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *nav; + + math_ensure(); + if (year < 0 || year > 4) + year = 0; + T = &math_tracks[year]; + + net_lesson_notes_ensure(L); + net_notes_target = L; + L->n_slides = n_slides; + + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_margin_start(page, 28); + gtk_widget_set_margin_end(page, 28); + gtk_widget_set_margin_top(page, 20); + gtk_widget_set_margin_bottom(page, 20); + + gtk_box_append(GTK_BOX(page), top_bar(T->map_page, title_key, sub_key)); + + scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_margin_top(scroll, 10); + gtk_box_append(GTK_BOX(page), scroll); + + L->stack = gtk_stack_new(); + gtk_stack_set_transition_type(GTK_STACK(L->stack), + GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), L->stack); + + for (guint s = 0; s < n_slides; s++) { + GtkWidget *stage = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + GtkWidget *card; + char name[16]; + + gtk_widget_set_vexpand(stage, TRUE); + g_snprintf(name, sizeof name, "s%u", s); + gtk_stack_add_named(GTK_STACK(L->stack), stage, name); + + card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16); + gtk_widget_add_css_class(card, "notes-card"); + gtk_widget_set_valign(card, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(stage), card); + + net_note_kicker(card, slides[s].kicker); + net_note_title(card, slides[s].title); + for (int i = 0; i < 8 && slides[s].line[i]; i++) + net_note_line(card, slides[s].line[i], FALSE); + if (slides[s].tip) + net_note_line(card, slides[s].tip, TRUE); + } + + gtk_widget_add_tick_callback(L->stack, math_note_tick, T, NULL); + T->note_last_w = -1; + math_rescale(T, 0); + net_notes_target = NULL; + + nav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); + gtk_box_set_homogeneous(GTK_BOX(nav), TRUE); + gtk_widget_set_margin_top(nav, 10); + gtk_box_append(GTK_BOX(page), nav); + + L->prev_btn = gtk_button_new_with_label(tr("net_slide_prev")); + gtk_widget_add_css_class(L->prev_btn, "btn-primary"); + g_signal_connect(L->prev_btn, "clicked", G_CALLBACK(math_slide_prev), L); + gtk_box_append(GTK_BOX(nav), L->prev_btn); + + L->next_btn = gtk_button_new_with_label(tr("net_slide_next")); + gtk_widget_add_css_class(L->next_btn, "btn-primary"); + g_signal_connect(L->next_btn, "clicked", G_CALLBACK(math_slide_next), L); + gtk_box_append(GTK_BOX(nav), L->next_btn); + + L->idx = 0; + math_slide_apply(L); + return page; +} + +void math_lessons_apply_lang(void) { + math_ensure(); + for (int y = 0; y < 5; y++) { + for (int i = 0; i < MATH_N; i++) + math_slide_apply(&math_tracks[y].lessons[i]); + } +} + +static void paint_hex(cairo_t *cr, unsigned hex) { + cairo_set_source_rgb(cr, ((hex >> 16) & 255) / 255.0, + ((hex >> 8) & 255) / 255.0, (hex & 255) / 255.0); +} + +static int board_px_w(void) { + return (int)(MATH_PAD * 2 + MATH_GX * MATH_CELL); +} + +static int board_px_h(void) { + return (int)(MATH_PAD * 2 + MATH_GY * MATH_CELL); +} + +static void board_to_screen(int gx, int gy, double *sx, double *sy) { + *sx = MATH_PAD + gx * MATH_CELL; + *sy = board_px_h() - MATH_PAD - gy * MATH_CELL; +} + +static gboolean board_snap(double x, double y, int *gx, int *gy) { + double cx = (x - MATH_PAD) / MATH_CELL; + double cy = (board_px_h() - MATH_PAD - y) / MATH_CELL; + int ix = (int)lround(cx); + int iy = (int)lround(cy); + + if (ix < 0 || ix > MATH_GX || iy < 0 || iy > MATH_GY) + return FALSE; + if (fabs(cx - ix) > 0.45 || fabs(cy - iy) > 0.45) + return FALSE; + *gx = ix; + *gy = iy; + return TRUE; +} + +static void board_draw(GtkDrawingArea *area, cairo_t *cr, int width, int height, + gpointer data) { + MathBoard *b = data; + int i; + static const char *names[] = {"A", "B", "C", "D"}; + + (void)area; + (void)width; + (void)height; + paint_hex(cr, app_theme.surface1); + cairo_rectangle(cr, 0, 0, board_px_w(), board_px_h()); + cairo_fill(cr); + + cairo_set_line_width(cr, 1); + cairo_set_source_rgba(cr, + ((app_theme.subtext >> 16) & 255) / 255.0, + ((app_theme.subtext >> 8) & 255) / 255.0, + (app_theme.subtext & 255) / 255.0, 0.55); + for (i = 0; i <= MATH_GX; i++) { + double x = MATH_PAD + i * MATH_CELL; + + cairo_move_to(cr, x, MATH_PAD); + cairo_line_to(cr, x, board_px_h() - MATH_PAD); + } + for (i = 0; i <= MATH_GY; i++) { + double y = MATH_PAD + i * MATH_CELL; + + cairo_move_to(cr, MATH_PAD, y); + cairo_line_to(cr, board_px_w() - MATH_PAD, y); + } + cairo_stroke(cr); + + paint_hex(cr, app_theme.text); + cairo_set_line_width(cr, 1.6); + cairo_move_to(cr, MATH_PAD, board_px_h() - MATH_PAD); + cairo_line_to(cr, board_px_w() - MATH_PAD, board_px_h() - MATH_PAD); + cairo_move_to(cr, MATH_PAD, board_px_h() - MATH_PAD); + cairo_line_to(cr, MATH_PAD, MATH_PAD); + cairo_stroke(cr); + + if (b->nf >= 2) { + double x0, y0, x1, y1; + + paint_hex(cr, app_theme.accent); + cairo_set_line_width(cr, 2); + board_to_screen(b->fx[0], b->fy[0], &x0, &y0); + board_to_screen(b->fx[1], b->fy[1], &x1, &y1); + cairo_move_to(cr, x0, y0); + cairo_line_to(cr, x1, y1); + if (b->nf >= 3) { + double x2, y2; + + board_to_screen(b->fx[2], b->fy[2], &x2, &y2); + cairo_move_to(cr, x0, y0); + cairo_line_to(cr, x2, y2); + } + cairo_stroke(cr); + } + + for (i = 0; i < b->nf && i < 4; i++) { + double x, y; + + board_to_screen(b->fx[i], b->fy[i], &x, &y); + paint_hex(cr, app_theme.accent); + cairo_arc(cr, x, y, 5.5, 0, 2 * G_PI); + cairo_fill(cr); + paint_hex(cr, app_theme.text); + cairo_set_font_size(cr, 13); + cairo_move_to(cr, x + 7, y - 7); + cairo_show_text(cr, names[i]); + } + for (i = 0; i < b->n; i++) { + double x, y; + char label[8]; + + board_to_screen(b->ux[i], b->uy[i], &x, &y); + if (b->checked) + paint_hex(cr, b->correct ? app_theme.success : app_theme.error); + else + paint_hex(cr, app_theme.success); + cairo_arc(cr, x, y, 5.5, 0, 2 * G_PI); + cairo_fill(cr); + paint_hex(cr, app_theme.text); + g_snprintf(label, sizeof label, "%d", i + 1); + cairo_set_font_size(cr, 13); + cairo_move_to(cr, x + 7, y + 14); + cairo_show_text(cr, label); + } +} + +static int board_has_fixed(MathBoard *b, int x, int y) { + for (int i = 0; i < b->nf; i++) + if (b->fx[i] == x && b->fy[i] == y) + return 1; + return 0; +} + +static void board_pressed(GtkGestureClick *gesture, int n_press, double x, double y, + gpointer data) { + MathBoard *b = data; + int gx, gy, i; + + (void)gesture; + (void)n_press; + if (!board_snap(x, y, &gx, &gy) || board_has_fixed(b, gx, gy)) + return; + for (i = 0; i < b->n; i++) { + if (b->ux[i] == gx && b->uy[i] == gy) { + memmove(&b->ux[i], &b->ux[i + 1], (b->n - i - 1) * sizeof(int)); + memmove(&b->uy[i], &b->uy[i + 1], (b->n - i - 1) * sizeof(int)); + b->n--; + b->checked = 0; + gtk_widget_queue_draw(b->area); + return; + } + } + if (b->n >= b->need || b->n >= MATH_PTS) + return; + b->ux[b->n] = gx; + b->uy[b->n] = gy; + b->n++; + b->checked = 0; + gtk_widget_queue_draw(b->area); +} + +static void board_undo(GtkButton *button, gpointer data) { + MathBoard *b = data; + + (void)button; + if (b->n <= 0) + return; + b->n--; + b->checked = 0; + gtk_widget_queue_draw(b->area); +} + +static void board_clear(GtkButton *button, gpointer data) { + MathBoard *b = data; + + (void)button; + b->n = 0; + b->checked = 0; + gtk_widget_queue_draw(b->area); +} + +static void math_ex_check(GtkButton *button, gpointer data) { + MathExCtx *ctx = data; + int ok = 0; + + (void)button; + for (int i = 0; i < ctx->n; i++) { + int good = 0; + + if (ctx->boards[i]) { + MathBoard *b = ctx->boards[i]; + + good = math_draw_ok(b->spec, b->ux, b->uy, b->n); + b->checked = 1; + b->correct = good; + gtk_widget_queue_draw(b->area); + } else if (ctx->entries[i]) { + const char *text = gtk_editable_get_text(GTK_EDITABLE(ctx->entries[i])); + + gtk_widget_remove_css_class(ctx->entries[i], "ok"); + gtk_widget_remove_css_class(ctx->entries[i], "wrong"); + good = math_answer_ok(ctx->items[i].answer, text); + gtk_widget_add_css_class(ctx->entries[i], good ? "ok" : "wrong"); + } + if (good) + ok++; + if (ctx->hints[i]) + gtk_widget_set_visible(ctx->hints[i], TRUE); + } + if (ok == ctx->n) { + set_feedback(ctx->feedback, TRUE, tr("feedback_ok")); + math_mark_done(ctx->track, ctx->lesson_id); + } else { + set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short")); + } +} + +GtkWidget *math_practice_page(int year, int lesson_id, const char *back_page, + const char *title_key, const char *heading_key, + const MathItem *items, int n) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *body; + GtkWidget *check; + GtkWidget *head; + GtkWidget *intro; + MathExCtx *ctx = g_new0(MathExCtx, 1); + + math_ensure(); + if (year < 0 || year > 4) + year = 0; + + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_margin_start(page, 32); + gtk_widget_set_margin_end(page, 32); + gtk_widget_set_margin_top(page, 24); + gtk_widget_set_margin_bottom(page, 24); + + gtk_box_append(GTK_BOX(page), top_bar(back_page, title_key, NULL)); + + scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_margin_top(scroll, 12); + gtk_box_append(GTK_BOX(page), scroll); + + body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16); + gtk_widget_set_hexpand(body, TRUE); + gtk_widget_set_margin_start(body, 4); + gtk_widget_set_margin_end(body, 4); + gtk_widget_set_margin_bottom(body, 12); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); + + head = gtk_label_new(NULL); + i18n_bind(head, heading_key, 0); + gtk_widget_set_halign(head, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(head), TRUE); + gtk_widget_add_css_class(head, "quiz-heading"); + gtk_box_append(GTK_BOX(body), head); + + intro = gtk_label_new(NULL); + i18n_bind(intro, "math_quiz_intro", 0); + gtk_widget_set_halign(intro, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(intro), TRUE); + gtk_label_set_xalign(GTK_LABEL(intro), 0); + gtk_widget_add_css_class(intro, "quiz-intro"); + gtk_box_append(GTK_BOX(body), intro); + + ctx->items = items; + ctx->n = n; + ctx->track = &math_tracks[year]; + ctx->lesson_id = lesson_id; + ctx->entries = g_new0(GtkWidget *, n); + ctx->boards = g_new0(MathBoard *, n); + ctx->hints = g_new0(GtkWidget *, n); + + for (int i = 0; i < n; i++) { + GtkWidget *card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + GtkWidget *prompt = gtk_label_new(items[i].prompt); + GtkWidget *hint = gtk_label_new(items[i].hint); + + gtk_widget_set_halign(card, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(body), card); + gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); + gtk_label_set_xalign(GTK_LABEL(prompt), 0); + gtk_widget_set_halign(prompt, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(card), prompt); + + if (math_is_draw(items[i].answer)) { + MathBoard *b = g_new0(MathBoard, 1); + GtkWidget *area = gtk_drawing_area_new(); + GtkGesture *click = gtk_gesture_click_new(); + GtkWidget *tools = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + GtkWidget *undo = gtk_button_new(); + GtkWidget *clear = gtk_button_new(); + GtkWidget *note = gtk_label_new(NULL); + + b->spec = items[i].answer; + b->need = math_draw_need(items[i].answer); + if (b->need < 1) + b->need = 1; + b->nf = math_draw_fixed(items[i].answer, b->fx, b->fy, 8); + b->area = area; + ctx->boards[i] = b; + + gtk_widget_set_size_request(area, board_px_w(), board_px_h()); + gtk_widget_set_halign(area, GTK_ALIGN_START); + gtk_drawing_area_set_content_width(GTK_DRAWING_AREA(area), board_px_w()); + gtk_drawing_area_set_content_height(GTK_DRAWING_AREA(area), board_px_h()); + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), board_draw, b, NULL); + gtk_widget_add_controller(area, GTK_EVENT_CONTROLLER(click)); + g_signal_connect(click, "pressed", G_CALLBACK(board_pressed), b); + gtk_box_append(GTK_BOX(card), area); + + i18n_bind(note, "math_draw_hint", 0); + gtk_label_set_wrap(GTK_LABEL(note), TRUE); + gtk_widget_add_css_class(note, "hint"); + gtk_widget_set_halign(note, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(card), note); + + i18n_bind(undo, "math_undo", 1); + i18n_bind(clear, "math_clear", 1); + g_signal_connect(undo, "clicked", G_CALLBACK(board_undo), b); + g_signal_connect(clear, "clicked", G_CALLBACK(board_clear), b); + gtk_box_append(GTK_BOX(tools), undo); + gtk_box_append(GTK_BOX(tools), clear); + gtk_box_append(GTK_BOX(card), tools); + } else { + GtkWidget *entry = gtk_entry_new(); + + gtk_entry_set_placeholder_text(GTK_ENTRY(entry), tr("math_answer_ph")); + gtk_widget_set_hexpand(entry, TRUE); + gtk_widget_set_size_request(entry, 280, -1); + gtk_box_append(GTK_BOX(card), entry); + ctx->entries[i] = entry; + } + + gtk_label_set_wrap(GTK_LABEL(hint), TRUE); + gtk_label_set_xalign(GTK_LABEL(hint), 0); + gtk_widget_set_halign(hint, GTK_ALIGN_START); + gtk_widget_add_css_class(hint, "hint"); + gtk_widget_set_visible(hint, FALSE); + gtk_box_append(GTK_BOX(card), hint); + ctx->hints[i] = hint; + } + + check = gtk_button_new(); + i18n_bind(check, "check", 1); + gtk_widget_add_css_class(check, "btn-primary"); + gtk_widget_set_halign(check, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(body), check); + g_signal_connect(check, "clicked", G_CALLBACK(math_ex_check), ctx); + + ctx->feedback = gtk_label_new(""); + gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(body), ctx->feedback); + return page; +} diff --git a/src/math_check.c b/src/math_check.c new file mode 100644 index 0000000..ce98d44 --- /dev/null +++ b/src/math_check.c @@ -0,0 +1,1362 @@ +/* Numeric and construction checker for mathematics. + * No GTK: the same rules are ported to Swift and Kotlin. + * A rational matches exactly, or within 0.0005. + * A square root also matches a decimal within 0.001. + * Pi stays symbolic: 3.14 is not accepted for pi. */ +#include +#include +#include +#include +#include +#include + +#define MC_TOKS 96 +#define MC_PARTS 8 +#define MC_PART 192 + +typedef struct { + int ok; + long an, ad; + long bn, bd; + int rad; + long pn, pd; +} Term; + +typedef enum { + TK_END, TK_NUM, TK_SQRT, TK_PI, TK_ADD, TK_SUB, TK_MUL, TK_DIV, TK_LP, TK_RP +} TokKind; + +typedef struct { + TokKind kind; + long n, d; +} Tok; + +static long igcd(long a, long b) { + if (a < 0) + a = -a; + if (b < 0) + b = -b; + while (b) { + long t = a % b; + a = b; + b = t; + } + return a ? a : 1; +} + +static int safe_mul(long a, long b, long *out) { + __int128 p = (__int128)a * (__int128)b; + + if (p > (__int128)LONG_MAX || p < (__int128)LONG_MIN) + return 0; + *out = (long)p; + return 1; +} + +static int set_frac(long n, long d, long *on, long *od) { + long g; + + if (d == 0) + return 0; + if (d < 0) { + n = -n; + d = -d; + } + g = igcd(n, d); + *on = n / g; + *od = d / g; + return 1; +} + +static int mul_frac(long n1, long d1, long n2, long d2, long *on, long *od) { + long n, d; + + if (!safe_mul(n1, n2, &n) || !safe_mul(d1, d2, &d)) + return 0; + return set_frac(n, d, on, od); +} + +static int add_frac(long n1, long d1, long n2, long d2, long *on, long *od) { + long a, b, den, s; + + if (!safe_mul(n1, d2, &a) || !safe_mul(n2, d1, &b) || !safe_mul(d1, d2, &den)) + return 0; + if ((b > 0 && a > LONG_MAX - b) || (b < 0 && a < LONG_MIN - b)) + return 0; + s = a + b; + return set_frac(s, den, on, od); +} + +static Term term_zero(void) { + Term t; + + memset(&t, 0, sizeof t); + t.ok = 1; + t.ad = t.bd = t.pd = 1; + return t; +} + +static int term_zero_p(Term t) { + return t.an == 0 && t.bn == 0 && t.pn == 0; +} + +static int norm_sqrt(Term *t) { + int r, f; + long pull, nb; + + if (t->bn == 0) { + t->bd = 1; + t->rad = 0; + return 1; + } + if (t->rad < 0) + return 0; + if (t->rad == 0) { + t->bn = 0; + t->bd = 1; + return 1; + } + if (t->rad > 1000000) + return 0; + r = t->rad; + pull = 1; + for (f = 2; f * f <= r; f++) { + int sq = f * f; + + while (r % sq == 0) { + r /= sq; + if (!safe_mul(pull, f, &pull)) + return 0; + } + } + if (!safe_mul(t->bn, pull, &nb)) + return 0; + t->bn = nb; + t->rad = r; + if (t->rad == 1 || t->rad == 0) { + if (!add_frac(t->an, t->ad, t->bn, t->bd, &t->an, &t->ad)) + return 0; + t->bn = 0; + t->bd = 1; + t->rad = 0; + } + if (t->bn == 0) { + t->bd = 1; + t->rad = 0; + } + return 1; +} + +static int term_add(Term a, Term b, Term *o) { + Term r = term_zero(); + + if (!a.ok || !b.ok) + return 0; + if (!add_frac(a.an, a.ad, b.an, b.ad, &r.an, &r.ad)) + return 0; + if (!add_frac(a.pn, a.pd, b.pn, b.pd, &r.pn, &r.pd)) + return 0; + if (a.bn == 0 && b.bn == 0) { + r.rad = 0; + } else if (a.bn == 0) { + r.bn = b.bn; + r.bd = b.bd; + r.rad = b.rad; + } else if (b.bn == 0) { + r.bn = a.bn; + r.bd = a.bd; + r.rad = a.rad; + } else if (a.rad != b.rad) { + return 0; + } else if (!add_frac(a.bn, a.bd, b.bn, b.bd, &r.bn, &r.bd)) { + return 0; + } else { + r.rad = a.rad; + if (r.bn == 0) { + r.bd = 1; + r.rad = 0; + } + } + *o = r; + return 1; +} + +static int add_sqrt_piece(Term *dst, long cn, long cd, int rad) { + Term piece = term_zero(); + + if (cn == 0 || rad <= 0) + return 1; + if (!set_frac(cn, cd, &piece.bn, &piece.bd)) + return 0; + piece.rad = rad; + if (!norm_sqrt(&piece)) + return 0; + return term_add(*dst, piece, dst); +} + +static int term_mul(Term a, Term b, Term *o) { + Term r = term_zero(); + long n, d, n2, d2; + + if (!a.ok || !b.ok) + return 0; + if ((a.pn && (b.bn || b.pn)) || (b.pn && (a.bn || a.pn))) + return 0; + if (!mul_frac(a.an, a.ad, b.an, b.ad, &r.an, &r.ad)) + return 0; + if (!mul_frac(a.an, a.ad, b.pn, b.pd, &n, &d)) + return 0; + if (!mul_frac(a.pn, a.pd, b.an, b.ad, &n2, &d2)) + return 0; + if (!add_frac(n, d, n2, d2, &r.pn, &r.pd)) + return 0; + if (b.bn && b.rad > 0) { + if (!mul_frac(a.an, a.ad, b.bn, b.bd, &n, &d)) + return 0; + if (!add_sqrt_piece(&r, n, d, b.rad)) + return 0; + } + if (a.bn && a.rad > 0) { + if (!mul_frac(b.an, b.ad, a.bn, a.bd, &n, &d)) + return 0; + if (!add_sqrt_piece(&r, n, d, a.rad)) + return 0; + } + if (a.bn && a.rad > 0 && b.bn && b.rad > 0) { + long radp; + + if (!mul_frac(a.bn, a.bd, b.bn, b.bd, &n, &d)) + return 0; + if (!safe_mul(a.rad, b.rad, &radp) || radp <= 0 || radp > 1000000) + return 0; + if (!add_sqrt_piece(&r, n, d, (int)radp)) + return 0; + } + *o = r; + return 1; +} + +static int term_div(Term a, Term b, Term *o) { + Term inv; + + if (!b.ok || term_zero_p(b)) + return 0; + if (b.bn == 0 && b.pn == 0) { + inv = term_zero(); + if (!set_frac(b.ad, b.an, &inv.an, &inv.ad)) + return 0; + return term_mul(a, inv, o); + } + if (b.an == 0 && b.pn == 0 && b.bn != 0 && b.rad > 1) { + long den; + + inv = term_zero(); + if (!safe_mul(b.bn, b.rad, &den)) + return 0; + if (!set_frac(b.bd, den, &inv.bn, &inv.bd)) + return 0; + inv.rad = b.rad; + if (!norm_sqrt(&inv)) + return 0; + return term_mul(a, inv, o); + } + if (b.an == 0 && b.bn == 0 && b.pn != 0) { + long n, d; + + if (a.an || a.bn) + return 0; + if (!mul_frac(a.pn, a.pd, b.pd, b.pn, &n, &d)) + return 0; + *o = term_zero(); + return set_frac(n, d, &o->an, &o->ad); + } + return 0; +} + +static int terms_exact(Term a, Term b) { + return a.ok && b.ok && a.an == b.an && a.ad == b.ad && a.bn == b.bn && + a.bd == b.bd && a.rad == b.rad && a.pn == b.pn && a.pd == b.pd; +} + +static double term_double(Term t) { + double v = (double)t.an / (double)t.ad + (double)t.pn / (double)t.pd * G_PI; + + if (t.bn && t.rad > 0) + v += (double)t.bn / (double)t.bd * sqrt((double)t.rad); + return v; +} + +static int terms_close(Term a, Term b) { + double diff; + + if (!a.ok || !b.ok) + return 0; + if (terms_exact(a, b)) + return 1; + if (a.pn || b.pn) + return 0; + diff = fabs(term_double(a) - term_double(b)); + if (a.bn || b.bn) + return diff <= 0.001; + return diff <= 0.0005; +} + +static int utf8_next(const char *s, int i, unsigned *cp) { + const unsigned char *u = (const unsigned char *)s; + unsigned c = u[i]; + + if (c < 0x80) { + *cp = c; + return 1; + } + if ((c & 0xE0) == 0xC0 && u[i + 1]) { + *cp = ((c & 0x1F) << 6) | (u[i + 1] & 0x3F); + return 2; + } + if ((c & 0xF0) == 0xE0 && u[i + 1] && u[i + 2]) { + *cp = ((c & 0x0F) << 12) | ((u[i + 1] & 0x3F) << 6) | (u[i + 2] & 0x3F); + return 3; + } + *cp = c; + return 1; +} + +static char fold_char(unsigned cp) { + switch (cp) { + case 0xE1: case 0xC1: return 'a'; + case 0x10D: case 0x10C: return 'c'; + case 0x10F: case 0x10E: return 'd'; + case 0xE9: case 0xC9: case 0x11B: case 0x11A: return 'e'; + case 0xED: case 0xCD: return 'i'; + case 0x148: case 0x147: return 'n'; + case 0xF3: case 0xD3: return 'o'; + case 0x159: case 0x158: return 'r'; + case 0x161: case 0x160: return 's'; + case 0x165: case 0x164: return 't'; + case 0xFA: case 0xDA: case 0x16F: case 0x16E: return 'u'; + case 0xFD: case 0xDD: return 'y'; + case 0x17E: case 0x17D: return 'z'; + default: + if (cp >= 'A' && cp <= 'Z') + return (char)(cp - 'A' + 'a'); + if (cp >= 'a' && cp <= 'z') + return (char)cp; + if (cp >= '0' && cp <= '9') + return (char)cp; + return 0; + } +} + +static void fold_word(const char *s, char *out, int cap) { + int i = 0, n = 0; + + while (s[i] && n + 1 < cap) { + unsigned cp; + int len = utf8_next(s, i, &cp); + char f = fold_char(cp); + + if (f) + out[n++] = f; + i += len > 0 ? len : 1; + } + out[n] = 0; +} + +static int word_ok(const char *alts, const char *user) { + char folded[128]; + char buf[256]; + char *save = NULL; + char *part; + + fold_word(user ? user : "", folded, sizeof folded); + if (!folded[0]) + return 0; + g_strlcpy(buf, alts ? alts : "", sizeof buf); + for (part = strtok_r(buf, "|", &save); part; part = strtok_r(NULL, "|", &save)) { + char one[128]; + + fold_word(part, one, sizeof one); + if (one[0] && strcmp(one, folded) == 0) + return 1; + } + return 0; +} + +static void strip_units(char *s) { + static const char *units[] = { + "procenta", "procent", "cm", "mm", "dm", "km", "m", NULL + }; + + for (;;) { + int n = (int)strlen(s); + int cut = 0; + unsigned cp; + int i; + + while (n > 0 && isspace((unsigned char)s[n - 1])) + s[--n] = 0; + if (n >= 2) { + utf8_next(s + n - 2, 0, &cp); + if (cp == 0xB0) { /* degree sign */ + s[n - 2] = 0; + continue; + } + } + if (n > 0 && s[n - 1] == '%') { + s[n - 1] = 0; + continue; + } + if (n > 0 && s[n - 1] == '.') { + s[n - 1] = 0; + continue; + } + for (i = 0; units[i]; i++) { + int m = (int)strlen(units[i]); + unsigned char prev; + + if (n <= m) + continue; + if (g_ascii_strncasecmp(s + n - m, units[i], m) != 0) + continue; + prev = (unsigned char)s[n - m - 1]; + if (g_ascii_isalpha(prev)) + continue; + s[n - m] = 0; + cut = 1; + break; + } + if (!cut) + break; + } +} + +static void trim_copy(const char *src, char *dst, int cap) { + int n; + + while (src && isspace((unsigned char)*src)) + src++; + g_strlcpy(dst, src ? src : "", cap); + n = (int)strlen(dst); + while (n > 0 && isspace((unsigned char)dst[n - 1])) + dst[--n] = 0; + if (n >= 2 && (dst[0] == '{' || dst[0] == '[' || dst[0] == '(') && + (dst[n - 1] == '}' || dst[n - 1] == ']' || dst[n - 1] == ')')) { + memmove(dst, dst + 1, n - 2); + dst[n - 2] = 0; + trim_copy(dst, dst, cap); + } +} + +static int lex_number(const char *s, int i, Tok *tok) { + int start = i; + long whole = 0, frac = 0, den = 1; + int neg = 0; + int saw = 0; + + if (s[i] == '+') + i++; + else if (s[i] == '-') { + neg = 1; + i++; + } + while (isdigit((unsigned char)s[i])) { + if (whole > LONG_MAX / 10) + return -1; + whole = whole * 10 + (s[i] - '0'); + saw = 1; + i++; + } + if (s[i] == '.' || s[i] == ',') { + i++; + while (isdigit((unsigned char)s[i])) { + if (frac > LONG_MAX / 10 || den > LONG_MAX / 10) + return -1; + frac = frac * 10 + (s[i] - '0'); + den *= 10; + saw = 1; + i++; + } + } + if (!saw || i == start) + return -1; + if (!safe_mul(whole, den, &whole)) + return -1; + if ((frac > 0 && whole > LONG_MAX - frac) || frac < 0) + return -1; + whole += frac; + if (neg) + whole = -whole; + tok->kind = TK_NUM; + if (!set_frac(whole, den, &tok->n, &tok->d)) + return -1; + return i; +} + +static int starts_ident(unsigned cp) { + return (cp >= 'A' && cp <= 'Z') || (cp >= 'a' && cp <= 'z') || cp == 0x221A || + cp == 0x3C0; +} + +static int lex(const char *s, Tok *toks, int cap) { + int i = 0, n = 0; + + while (s[i]) { + unsigned cp; + int len; + Tok tok; + + if (n + 2 >= cap) + return -1; + if (isspace((unsigned char)s[i])) { + i++; + continue; + } + memset(&tok, 0, sizeof tok); + tok.d = 1; + if (isdigit((unsigned char)s[i]) || + ((s[i] == '.' || s[i] == ',') && isdigit((unsigned char)s[i + 1]))) { + int ni = lex_number(s, i, &tok); + + if (ni < 0) + return -1; + toks[n++] = tok; + i = ni; + continue; + } + len = utf8_next(s, i, &cp); + if (cp == '+' || cp == '-' || cp == 0x2212 || cp == 0x2013) { + tok.kind = cp == '+' ? TK_ADD : TK_SUB; + toks[n++] = tok; + i += cp < 128 ? 1 : len; + continue; + } + if (cp == '*' || cp == 0xD7 || cp == 0xB7) { + tok.kind = TK_MUL; + toks[n++] = tok; + i += cp < 128 ? 1 : len; + continue; + } + if (cp == '/' || cp == ':') { + tok.kind = TK_DIV; + toks[n++] = tok; + i++; + continue; + } + if (cp == '(') { + tok.kind = TK_LP; + toks[n++] = tok; + i++; + continue; + } + if (cp == ')') { + tok.kind = TK_RP; + toks[n++] = tok; + i++; + continue; + } + if (cp == 0x221A) { + tok.kind = TK_SQRT; + toks[n++] = tok; + i += len; + continue; + } + if (cp == 0x3C0) { + tok.kind = TK_PI; + toks[n++] = tok; + i += len; + continue; + } + if (starts_ident(cp)) { + char ident[16]; + int k = 0; + + while (s[i] && k + 1 < (int)sizeof ident) { + unsigned c2; + int l2 = utf8_next(s, i, &c2); + + if (!((c2 >= 'A' && c2 <= 'Z') || (c2 >= 'a' && c2 <= 'z'))) + break; + ident[k++] = (char)((c2 >= 'A' && c2 <= 'Z') ? c2 - 'A' + 'a' : c2); + i += l2; + } + ident[k] = 0; + if (strcmp(ident, "sqrt") == 0) + tok.kind = TK_SQRT; + else if (strcmp(ident, "pi") == 0) + tok.kind = TK_PI; + else + return -1; + toks[n++] = tok; + continue; + } + return -1; + } + toks[n].kind = TK_END; + toks[n].d = 1; + return n; +} + +typedef struct { + const Tok *t; + int i; + int n; + int bad; +} Parse; + +static Term parse_expr(Parse *p); + +static int at_primary(const Tok *t) { + return t->kind == TK_NUM || t->kind == TK_SQRT || t->kind == TK_PI || + t->kind == TK_LP; +} + +static Term parse_primary(Parse *p) { + Term r = term_zero(); + Tok tok; + + if (p->bad || p->i >= p->n) { + p->bad = 1; + r.ok = 0; + return r; + } + tok = p->t[p->i]; + if (tok.kind == TK_NUM) { + /* Mixed number: 1 1/2 means 1 + 1/2. A leading minus is unary. */ + if (p->i + 3 < p->n && p->t[p->i + 1].kind == TK_NUM && + p->t[p->i + 2].kind == TK_DIV && p->t[p->i + 3].kind == TK_NUM && + p->t[p->i + 1].d == 1 && p->t[p->i + 3].d == 1 && tok.d == 1) { + Term whole = term_zero(); + Term frac = term_zero(); + + whole.an = tok.n; + whole.ad = tok.d; + if (!set_frac(p->t[p->i + 1].n, p->t[p->i + 3].n, &frac.an, &frac.ad)) { + p->bad = 1; + r.ok = 0; + return r; + } + if (!term_add(whole, frac, &r)) { + p->bad = 1; + r.ok = 0; + return r; + } + p->i += 4; + return r; + } + r.an = tok.n; + r.ad = tok.d; + p->i++; + return r; + } + if (tok.kind == TK_PI) { + r.pn = 1; + r.pd = 1; + p->i++; + return r; + } + if (tok.kind == TK_SQRT) { + Term inner; + + p->i++; + /* √2 and sqrt2 are the root of the following whole number. */ + if (p->i < p->n && p->t[p->i].kind == TK_NUM && p->t[p->i].d == 1 && + p->t[p->i].n >= 0) { + r.bn = 1; + r.bd = 1; + r.rad = (int)p->t[p->i].n; + p->i++; + if (!norm_sqrt(&r)) { + p->bad = 1; + r.ok = 0; + } + return r; + } + if (p->i >= p->n || p->t[p->i].kind != TK_LP) { + p->bad = 1; + r.ok = 0; + return r; + } + p->i++; + inner = parse_expr(p); + if (p->bad || p->i >= p->n || p->t[p->i].kind != TK_RP || !inner.ok || + inner.bn || inner.pn || inner.an < 0) { + p->bad = 1; + r.ok = 0; + return r; + } + p->i++; + if (inner.ad != 1) { + /* sqrt of a fraction is not asked; reject rather than guess. */ + p->bad = 1; + r.ok = 0; + return r; + } + r.bn = 1; + r.bd = 1; + r.rad = (int)inner.an; + if (!norm_sqrt(&r)) { + p->bad = 1; + r.ok = 0; + } + return r; + } + if (tok.kind == TK_LP) { + p->i++; + r = parse_expr(p); + if (p->bad || p->i >= p->n || p->t[p->i].kind != TK_RP) { + p->bad = 1; + r.ok = 0; + return r; + } + p->i++; + return r; + } + p->bad = 1; + r.ok = 0; + return r; +} + +static Term parse_unary(Parse *p) { + if (p->i < p->n && (p->t[p->i].kind == TK_ADD || p->t[p->i].kind == TK_SUB)) { + int neg = p->t[p->i].kind == TK_SUB; + Term r; + + p->i++; + r = parse_unary(p); + if (!r.ok) + return r; + if (neg) { + r.an = -r.an; + r.bn = -r.bn; + r.pn = -r.pn; + } + return r; + } + return parse_primary(p); +} + +static Term parse_term(Parse *p) { + Term left = parse_unary(p); + + if (!left.ok) + return left; + while (!p->bad && p->i < p->n) { + TokKind k = p->t[p->i].kind; + Term right; + int juxt = 0; + + if (k != TK_MUL && k != TK_DIV) { + if (at_primary(&p->t[p->i]) && + (p->t[p->i].kind == TK_SQRT || p->t[p->i].kind == TK_PI || + p->t[p->i].kind == TK_LP)) + juxt = 1; + else + break; + } + if (!juxt) + p->i++; + right = parse_unary(p); + if (!right.ok) { + left.ok = 0; + return left; + } + if (k == TK_DIV && !juxt) { + if (!term_div(left, right, &left)) { + p->bad = 1; + left.ok = 0; + return left; + } + } else if (!term_mul(left, right, &left)) { + p->bad = 1; + left.ok = 0; + return left; + } + } + return left; +} + +static Term parse_expr(Parse *p) { + Term left = parse_term(p); + + while (!p->bad && left.ok && p->i < p->n && + (p->t[p->i].kind == TK_ADD || p->t[p->i].kind == TK_SUB)) { + int sub = p->t[p->i].kind == TK_SUB; + Term right; + + p->i++; + right = parse_term(p); + if (!right.ok) { + left.ok = 0; + return left; + } + if (sub) { + right.an = -right.an; + right.bn = -right.bn; + right.pn = -right.pn; + } + if (!term_add(left, right, &left)) { + p->bad = 1; + left.ok = 0; + return left; + } + } + return left; +} + +static int parse_term_text(const char *text, Term *out) { + char buf[MC_PART]; + Tok toks[MC_TOKS]; + Parse p; + int n; + + trim_copy(text, buf, sizeof buf); + strip_units(buf); + trim_copy(buf, buf, sizeof buf); + if (!buf[0]) + return 0; + n = lex(buf, toks, MC_TOKS); + if (n < 0) + return 0; + p.t = toks; + p.i = 0; + p.n = n; + p.bad = 0; + *out = parse_expr(&p); + if (p.bad || !out->ok || p.i != n) + return 0; + return 1; +} + +static int split_parts(const char *text, char parts[][MC_PART], int max) { + char buf[512]; + int n = 0; + const char *s; + int use_semi; + + trim_copy(text, buf, sizeof buf); + if (!buf[0]) + return -1; + use_semi = strchr(buf, ';') != NULL; + s = buf; + while (*s && n < max) { + const char *end = s; + int len; + + if (use_semi) { + end = strchr(s, ';'); + if (!end) + end = s + strlen(s); + } else { + end = strstr(s, ", "); + if (!end) + end = s + strlen(s); + } + len = (int)(end - s); + if (len >= MC_PART) + return -1; + memcpy(parts[n], s, len); + parts[n][len] = 0; + trim_copy(parts[n], parts[n], MC_PART); + if (!parts[n][0]) + return -1; + n++; + s = end; + if (*s == 0) + break; + s += use_semi ? 1 : 2; + } + if (*s) + return -1; + return n; +} + +static int num_ok(const char *spec, const char *user) { + Term a, b; + + if (!parse_term_text(spec, &a) || !parse_term_text(user, &b)) + return 0; + return terms_close(a, b); +} + +static int list_ok(const char *spec, const char *user, int ordered) { + char sp[MC_PARTS][MC_PART]; + char up[MC_PARTS][MC_PART]; + Term st[MC_PARTS], ut[MC_PARTS]; + int ns, nu, i, j; + int used[MC_PARTS]; + + ns = split_parts(spec, sp, MC_PARTS); + nu = split_parts(user, up, MC_PARTS); + if (ns < 1 || ns != nu) + return 0; + for (i = 0; i < ns; i++) { + if (!parse_term_text(sp[i], &st[i]) || !parse_term_text(up[i], &ut[i])) + return 0; + used[i] = 0; + } + if (ordered) { + for (i = 0; i < ns; i++) + if (!terms_close(st[i], ut[i])) + return 0; + return 1; + } + for (i = 0; i < ns; i++) { + int hit = -1; + + for (j = 0; j < nu; j++) { + if (!used[j] && terms_exact(st[i], ut[j])) { + hit = j; + break; + } + } + if (hit < 0) { + for (j = 0; j < nu; j++) { + if (!used[j] && terms_close(st[i], ut[j])) { + hit = j; + break; + } + } + } + if (hit < 0) + return 0; + used[hit] = 1; + } + return 1; +} + +gboolean math_answer_ok(const char *spec, const char *user) { + if (!spec || !user) + return FALSE; + while (*spec && isspace((unsigned char)*spec)) + spec++; + if (g_str_has_prefix(spec, "word:")) + return word_ok(spec + 5, user); + if (g_str_has_prefix(spec, "set:")) + return list_ok(spec + 4, user, 0); + if (g_str_has_prefix(spec, "pair:")) + return list_ok(spec + 5, user, 1); + if (g_str_has_prefix(spec, "num:")) + return num_ok(spec + 4, user); + return FALSE; +} + +static int on_board(int x, int y) { + return x >= 0 && x <= 10 && y >= 0 && y <= 8; +} + +static int parse_xy(const char **ps, int *x, int *y) { + char *end; + long vx, vy; + const char *s = *ps; + + while (*s && (isspace((unsigned char)*s) || *s == ';')) + s++; + if (!*s) + return 0; + vx = strtol(s, &end, 10); + if (end == s || *end != ',') + return 0; + s = end + 1; + vy = strtol(s, &end, 10); + if (end == s) + return 0; + if (vx < -100 || vx > 100 || vy < -100 || vy > 100) + return 0; + *x = (int)vx; + *y = (int)vy; + *ps = end; + return 1; +} + +static const char *draw_body(const char *spec, const char *kind) { + int n = (int)strlen(kind); + + if (!spec || strncmp(spec, kind, n) != 0 || spec[n] != ':') + return NULL; + return spec + n + 1; +} + +gboolean math_is_draw(const char *spec) { + return draw_body(spec, "square") || draw_body(spec, "mid") || + draw_body(spec, "right") || draw_body(spec, "para") || + draw_body(spec, "points"); +} + +static int points_scan(const char *body, int *fx, int *fy, int *fn, + int *rx, int *ry, int *rn, int cap) { + const char *s = body; + + *fn = 0; + *rn = 0; + while (*s) { + int fix = 0; + int x, y; + + while (*s && (isspace((unsigned char)*s) || *s == ';')) + s++; + if (!*s) + break; + if (g_str_has_prefix(s, "fix:")) { + fix = 1; + s += 4; + } + if (!parse_xy(&s, &x, &y)) + return 0; + if (fix) { + if (*fn >= cap) + return 0; + fx[*fn] = x; + fy[*fn] = y; + (*fn)++; + } else { + if (*rn >= cap) + return 0; + rx[*rn] = x; + ry[*rn] = y; + (*rn)++; + } + } + return 1; +} + +int math_draw_need(const char *spec) { + int fx[8], fy[8], rx[8], ry[8], fn = 0, rn = 0; + + if (draw_body(spec, "square")) + return 2; + if (draw_body(spec, "mid") || draw_body(spec, "right") || draw_body(spec, "para")) + return 1; + if (draw_body(spec, "points")) { + if (!points_scan(draw_body(spec, "points"), fx, fy, &fn, rx, ry, &rn, 8)) + return -1; + return rn; + } + return -1; +} + +int math_draw_fixed(const char *spec, int *xs, int *ys, int cap) { + const char *body; + int n = 0; + int x, y; + + if (!xs || !ys || cap <= 0) + return 0; + if ((body = draw_body(spec, "square")) || (body = draw_body(spec, "mid")) || + (body = draw_body(spec, "right")) || (body = draw_body(spec, "para"))) { + const char *s = body; + + while (*s && n < cap) { + while (*s && (isspace((unsigned char)*s) || *s == ';')) + s++; + if (!*s) + break; + if (!parse_xy(&s, &x, &y)) + return 0; + xs[n] = x; + ys[n] = y; + n++; + } + return n; + } + if ((body = draw_body(spec, "points"))) { + int fx[8], fy[8], rx[8], ry[8], fn = 0, rn = 0, i; + + if (!points_scan(body, fx, fy, &fn, rx, ry, &rn, 8)) + return 0; + for (i = 0; i < fn && i < cap; i++) { + xs[i] = fx[i]; + ys[i] = fy[i]; + } + return fn < cap ? fn : cap; + } + return 0; +} + +static int same_pair(int ax, int ay, int bx, int by, int cx, int cy, int dx, int dy) { + return (ax == cx && ay == cy && bx == dx && by == dy) || + (ax == dx && ay == dy && bx == cx && by == cy); +} + +static int user_ok_board(const int *xs, const int *ys, int n) { + int i, j; + + for (i = 0; i < n; i++) { + if (!on_board(xs[i], ys[i])) + return 0; + for (j = i + 1; j < n; j++) + if (xs[i] == xs[j] && ys[i] == ys[j]) + return 0; + } + return 1; +} + +gboolean math_draw_ok(const char *spec, const int *xs, const int *ys, int n) { + const char *body; + int ax, ay, bx, by, cx, cy; + const char *s; + + if (!spec || n < 0 || (n > 0 && (!xs || !ys))) + return FALSE; + if (!user_ok_board(xs, ys, n)) + return FALSE; + if ((body = draw_body(spec, "square"))) { + int dx, dy; + int p1x, p1y, q1x, q1y, p2x, p2y, q2x, q2y; + + s = body; + if (n != 2 || !parse_xy(&s, &ax, &ay) || !parse_xy(&s, &bx, &by)) + return FALSE; + dx = bx - ax; + dy = by - ay; + if (dx == 0 && dy == 0) + return FALSE; + p1x = bx - dy; + p1y = by + dx; + q1x = ax - dy; + q1y = ay + dx; + p2x = bx + dy; + p2y = by - dx; + q2x = ax + dy; + q2y = ay - dx; + if (on_board(p1x, p1y) && on_board(q1x, q1y) && + same_pair(xs[0], ys[0], xs[1], ys[1], p1x, p1y, q1x, q1y)) + return TRUE; + if (on_board(p2x, p2y) && on_board(q2x, q2y) && + same_pair(xs[0], ys[0], xs[1], ys[1], p2x, p2y, q2x, q2y)) + return TRUE; + return FALSE; + } + if ((body = draw_body(spec, "mid"))) { + s = body; + if (n != 1 || !parse_xy(&s, &ax, &ay) || !parse_xy(&s, &bx, &by)) + return FALSE; + if (((ax + bx) & 1) || ((ay + by) & 1)) + return FALSE; + return xs[0] == (ax + bx) / 2 && ys[0] == (ay + by) / 2 && on_board(xs[0], ys[0]); + } + if ((body = draw_body(spec, "right"))) { + long v1x, v1y, v2x, v2y; + + s = body; + if (n != 1 || !parse_xy(&s, &ax, &ay) || !parse_xy(&s, &bx, &by)) + return FALSE; + cx = xs[0]; + cy = ys[0]; + if ((cx == ax && cy == ay) || (cx == bx && cy == by)) + return FALSE; + v1x = ax - cx; + v1y = ay - cy; + v2x = bx - cx; + v2y = by - cy; + return v1x * v2x + v1y * v2y == 0; + } + if ((body = draw_body(spec, "para"))) { + int dx, dy; + + s = body; + if (n != 1 || !parse_xy(&s, &ax, &ay) || !parse_xy(&s, &bx, &by) || + !parse_xy(&s, &cx, &cy)) + return FALSE; + dx = bx + cx - ax; + dy = by + cy - ay; + return xs[0] == dx && ys[0] == dy && on_board(dx, dy); + } + if ((body = draw_body(spec, "points"))) { + int fx[8], fy[8], rx[8], ry[8], fn = 0, rn = 0, i, j; + int used[8]; + + if (!points_scan(body, fx, fy, &fn, rx, ry, &rn, 8) || n != rn) + return FALSE; + for (i = 0; i < n; i++) + used[i] = 0; + for (i = 0; i < rn; i++) { + int hit = 0; + + for (j = 0; j < n; j++) { + if (!used[j] && xs[j] == rx[i] && ys[j] == ry[i]) { + used[j] = 1; + hit = 1; + break; + } + } + if (!hit) + return FALSE; + } + return TRUE; + } + return FALSE; +} + +#ifdef MATH_CHECK_TEST +static int fails; + +static void expect(const char *spec, const char *user, int want) { + int got = math_answer_ok(spec, user); + + if (got != want) { + fprintf(stderr, "FAIL %s vs %s got %d want %d\n", spec, user, got, want); + fails++; + } +} + +static void expect_draw(const char *spec, const int *xs, const int *ys, int n, int want) { + int got = math_draw_ok(spec, xs, ys, n); + + if (got != want) { + fprintf(stderr, "FAIL draw %s got %d want %d\n", spec, got, want); + fails++; + } +} + +static int run_file(const char *path) { + FILE *f = fopen(path, "r"); + char line[512]; + + if (!f) { + perror(path); + return 1; + } + while (fgets(line, sizeof line, f)) { + char *spec, *user, *flag, *nl; + int want; + + if (line[0] == '#' || line[0] == '\n') + continue; + nl = strchr(line, '\n'); + if (nl) + *nl = 0; + spec = line; + user = strchr(spec, '\t'); + if (!user) + continue; + *user++ = 0; + flag = strchr(user, '\t'); + if (!flag) + continue; + *flag++ = 0; + want = atoi(flag); + if (g_str_has_prefix(spec, "draw ")) { + int xs[8], ys[8], n = 0; + const char *s = user; + + while (*s && n < 8) { + while (*s == ' ' || *s == ';') + s++; + if (!*s) + break; + if (!parse_xy(&s, &xs[n], &ys[n])) { + fprintf(stderr, "bad draw user %s\n", user); + fails++; + n = -1; + break; + } + n++; + } + if (n >= 0) + expect_draw(spec + 5, xs, ys, n, want); + } else { + expect(spec, user, want); + } + } + fclose(f); + return fails ? 1 : 0; +} + +int main(int argc, char **argv) { + const int sq_hi[] = {5, 2}; + const int sq_hi_y[] = {6, 6}; + const int sq_lo[] = {5, 2}; + const int sq_lo_y[] = {0, 0}; + const int badpt[] = {5, 2}; + const int badpt_y[] = {6, 5}; + const int midp[] = {4}; + const int midy[] = {3}; + const int th1[] = {5}; + const int th1y[] = {7}; + const int th2[] = {5}; + const int th2y[] = {1}; + const int thbad[] = {5}; + const int thbady[] = {4}; + const int par[] = {5}; + const int pary[] = {3}; + const int pt[] = {1}; + const int pty[] = {4}; + const int ptoff[] = {1}; + const int ptoffy[] = {-2}; + + expect("num:5/6", "5/6", 1); + expect("num:5/6", "0,833", 1); + expect("num:5/6", "0.833", 1); + expect("num:5/6", "0,83", 0); + expect("num:-3/4", "-3/4", 1); + expect("num:-3/4", "-(3/4)", 1); + expect("num:3/4", "0,75", 1); + expect("num:3/4", "0.75", 1); + expect("num:14*pi", "14pi", 1); + expect("num:14*pi", "14*pi", 1); + expect("num:14*pi", "14π", 1); + expect("num:14*pi", "43,98", 0); + expect("num:14*pi", "44", 0); + expect("num:pi", "3,14", 0); + expect("num:pi", "22/7", 0); + expect("num:pi", "3", 0); + expect("num:2*sqrt(3)", "2√3", 1); + expect("num:2*sqrt(3)", "sqrt(12)", 1); + expect("num:sqrt(12)", "2*sqrt(3)", 1); + expect("num:3/2", "1 1/2", 1); + expect("num:sqrt(2)/2", "√2/2", 1); + expect("num:sqrt(2)/2", "1/sqrt(2)", 1); + expect("num:sqrt(2)", "1,414", 1); + expect("num:sqrt(2)", "1.414", 1); + expect("num:sqrt(2)", "1,41", 0); + expect("num:1/3", "0,333", 1); + expect("num:1/3", "0,33", 0); + expect("num:12", "12 cm", 1); + expect("num:25", "25%", 1); + expect("num:25", "25 %", 1); + expect("num:50", "50 m", 1); + expect("num:70", "70°", 1); + expect("num:0", "0", 1); + expect("num:14", "2+3*4", 1); + expect("num:8/3", "8/3", 1); + expect("num:8/3", "2,667", 1); + expect("num:8/3", "2,66", 0); + expect("num:-1", "-1", 1); + expect("set:2;3", "3;2", 1); + expect("set:2;3", "2; 3", 1); + expect("set:-2;2", "2;-2", 1); + expect("set:2;3", "2;4", 0); + expect("pair:6;4", "6;4", 1); + expect("pair:6;4", "6; 4", 1); + expect("pair:6;4", "4;6", 0); + expect("pair:1;-2", "1;-2", 1); + expect("word:pravouhly", "pravoúhlý", 1); + expect("word:ano|ne", "Ano", 1); + expect("word:ano|ne", "ne", 1); + expect("word:ano", "ne", 0); + expect("num:5/6", "", 0); + expect("num:5/6", "abc", 0); + + expect_draw("square:2,3;5,3", sq_hi, sq_hi_y, 2, 1); + expect_draw("square:2,3;5,3", sq_lo, sq_lo_y, 2, 1); + expect_draw("square:2,3;5,3", badpt, badpt_y, 2, 0); + expect_draw("mid:1,2;7,4", midp, midy, 1, 1); + expect_draw("right:2,4;8,4", th1, th1y, 1, 1); + expect_draw("right:2,4;8,4", th2, th2y, 1, 1); + expect_draw("right:2,4;8,4", thbad, thbady, 1, 0); + expect_draw("para:1,1;4,1;2,3", par, pary, 1, 1); + expect_draw("points:1,4;fix:1,1;fix:5,1", pt, pty, 1, 1); + expect_draw("points:1,4;fix:1,1;fix:5,1", ptoff, ptoffy, 1, 0); + if (math_draw_need("square:2,3;5,3") != 2 || math_draw_need("right:2,4;8,4") != 1) + fails++; + + if (argc > 1) + return run_file(argv[1]) || fails; + if (fails) { + fprintf(stderr, "%d failed\n", fails); + return 1; + } + printf("math_check ok\n"); + return 0; +} +#endif diff --git a/src/math_i18n.inc b/src/math_i18n.inc new file mode 100644 index 0000000..6fecfaf --- /dev/null +++ b/src/math_i18n.inc @@ -0,0 +1,176 @@ + {"mat_years_sub", "Opakování základní školy a čtyři ročníky gymnázia.", "Elementary review and four years of gymnasium mathematics."}, + {"math_quiz_intro", "Napište výsledek. Zlomky jako 3/4, desetinnou čárku i tečku, π jako pi, odmocninu jako sqrt(2) nebo √2. Více čísel oddělte středníkem.", "Write the result. Fractions as 3/4, a decimal comma or point, π as pi, a root as sqrt(2) or √2. Separate several numbers with a semicolon."}, + {"math_answer_ph", "výsledek", "result"}, + {"math_undo", "Zpět", "Undo"}, + {"math_clear", "Smazat body", "Clear points"}, + {"math_draw_hint", "Klepněte na mřížku. Osa y směřuje nahoru.", "Tap the grid. The y-axis points up."}, + {"mat0_year", "Opakování základní školy", "Elementary-school review"}, + {"mat0_sub", "Pořadí operací, zlomky, procenta, rovnice a rýsování.", "Order of operations, fractions, percentages, equations and a construction."}, + {"mat0_unit1", "Pořadí operací", "Order of operations"}, + {"mat0_unit1_sub", "Závorky, násobení a dělení, sčítání a odčítání.", "Brackets, then multiplication and division, then addition and subtraction."}, + {"mat0_ex1_title", "Příklady", "Practice"}, + {"mat0_quiz1_head", "Pořadí operací", "Order of operations"}, + {"mat0_unit2", "Zlomky", "Fractions"}, + {"mat0_unit2_sub", "Sčítání, odčítání, násobení a dělení zlomků.", "Adding, subtracting, multiplying and dividing fractions."}, + {"mat0_ex2_title", "Příklady", "Practice"}, + {"mat0_quiz2_head", "Zlomky", "Fractions"}, + {"mat0_unit3", "Desetinná čísla", "Decimals"}, + {"mat0_unit3_sub", "Sčítání, násobení a dělení desetinných čísel.", "Adding, multiplying and dividing decimals."}, + {"mat0_ex3_title", "Příklady", "Practice"}, + {"mat0_quiz3_head", "Desetinná čísla", "Decimals"}, + {"mat0_unit4", "Procenta", "Percentages"}, + {"mat0_unit4_sub", "Procento ze základu, počet procent a cena po slevě.", "A percentage of a base, the rate, and a price after a discount."}, + {"mat0_ex4_title", "Příklady", "Practice"}, + {"mat0_quiz4_head", "Procenta", "Percentages"}, + {"mat0_unit5", "Poměr a měřítko", "Ratio and scale"}, + {"mat0_unit5_sub", "Dělení v poměru, úměra a měřítko mapy.", "Sharing in a ratio, proportion and the scale of a map."}, + {"mat0_ex5_title", "Příklady", "Practice"}, + {"mat0_quiz5_head", "Poměr a měřítko", "Ratio and scale"}, + {"mat0_unit6", "Záporná čísla", "Negative numbers"}, + {"mat0_unit6_sub", "Sčítání, násobení a absolutní hodnota.", "Addition, multiplication and absolute value."}, + {"mat0_ex6_title", "Příklady", "Practice"}, + {"mat0_quiz6_head", "Záporná čísla", "Negative numbers"}, + {"mat0_unit7", "Lineární rovnice", "Linear equations"}, + {"mat0_unit7_sub", "Ekvivalentní úpravy a jednoduché rovnice.", "Equivalent steps and simple equations."}, + {"mat0_ex7_title", "Příklady", "Practice"}, + {"mat0_quiz7_head", "Lineární rovnice", "Linear equations"}, + {"mat0_unit8", "Obvod, obsah a čtverec", "Perimeter, area and a square"}, + {"mat0_unit8_sub", "Obdélník, trojúhelník a doplnění čtverce na mřížce.", "A rectangle, a triangle, and completing a square on the grid."}, + {"mat0_ex8_title", "Příklady", "Practice"}, + {"mat0_quiz8_head", "Obvod, obsah a čtverec", "Perimeter, area and a square"}, + {"mat_year1", "1. ročník", "Year 1"}, + {"mat_sub", "Dělitelnost, výrazy, rovnice, planimetrie a konstrukce.", "Divisibility, expressions, equations, plane figures and a construction."}, + {"mat_unit1", "Dělitelnost a prvočísla", "Divisibility and primes"}, + {"mat_unit1_sub", "Největší společný dělitel, násobek a rozklad.", "Highest common factor, lowest common multiple and factorisation."}, + {"mat_ex1_title", "Příklady", "Practice"}, + {"mat_quiz1_head", "Dělitelnost a prvočísla", "Divisibility and primes"}, + {"mat_unit2", "Mocniny a odmocniny", "Powers and roots"}, + {"mat_unit2_sub", "Mocniny se stejným základem a druhá odmocnina.", "Powers with the same base and the square root."}, + {"mat_ex2_title", "Příklady", "Practice"}, + {"mat_quiz2_head", "Mocniny a odmocniny", "Powers and roots"}, + {"mat_unit3", "Algebraické výrazy", "Algebraic expressions"}, + {"mat_unit3_sub", "Dosazení, druhá mocnina součtu a rozklad rozdílu čtverců.", "Substitution, the square of a sum and a difference of squares."}, + {"mat_ex3_title", "Příklady", "Practice"}, + {"mat_quiz3_head", "Algebraické výrazy", "Algebraic expressions"}, + {"mat_unit4", "Lineární rovnice s úpravami", "Linear equations"}, + {"mat_unit4_sub", "Rovnice s koeficientem, závorkou a zlomkem.", "Equations with a coefficient, a bracket and a fraction."}, + {"mat_ex4_title", "Příklady", "Practice"}, + {"mat_quiz4_head", "Lineární rovnice s úpravami", "Linear equations"}, + {"mat_unit5", "Slovní úlohy", "Word problems"}, + {"mat_unit5_sub", "Dráha, společná práce, směs a sleva.", "Distance, joint work, a mixture and a discount."}, + {"mat_ex5_title", "Příklady", "Practice"}, + {"mat_quiz5_head", "Slovní úlohy", "Word problems"}, + {"mat_unit6", "Planimetrie", "Plane figures"}, + {"mat_unit6_sub", "Úhly trojúhelníku, vnější úhel a pravidelný mnohoúhelník.", "Angles of a triangle, the exterior angle and a regular polygon."}, + {"mat_ex6_title", "Příklady", "Practice"}, + {"mat_quiz6_head", "Planimetrie", "Plane figures"}, + {"mat_unit7", "Obvody a obsahy", "Perimeter and area"}, + {"mat_unit7_sub", "Kružnice, lichoběžník a Pythagorova věta.", "The circle, the trapezium and Pythagoras."}, + {"mat_ex7_title", "Příklady", "Practice"}, + {"mat_quiz7_head", "Obvody a obsahy", "Perimeter and area"}, + {"mat_unit8", "Konstrukce trojúhelníku", "Constructing a triangle"}, + {"mat_unit8_sub", "Vzdálenost bodů a trojúhelník daný třemi stranami.", "The distance between points and a triangle given by three sides."}, + {"mat_ex8_title", "Příklady", "Practice"}, + {"mat_quiz8_head", "Konstrukce trojúhelníku", "Constructing a triangle"}, + {"mat_year2", "2. ročník", "Year 2"}, + {"mat2_sub", "Funkce, soustavy, kvadratické rovnice, goniometrie a kružnice.", "Functions, systems, quadratic equations, trigonometry and circles."}, + {"mat2_unit1", "Lineární funkce", "Linear functions"}, + {"mat2_unit1_sub", "Předpis, průsečík s osou y, směrnice a nulový bod.", "The formula, the y-intercept, the slope and a root."}, + {"mat2_ex1_title", "Příklady", "Practice"}, + {"mat2_quiz1_head", "Lineární funkce", "Linear functions"}, + {"mat2_unit2", "Soustavy rovnic", "Systems of equations"}, + {"mat2_unit2_sub", "Dvě lineární rovnice o dvou neznámých.", "Two linear equations in two unknowns."}, + {"mat2_ex2_title", "Příklady", "Practice"}, + {"mat2_quiz2_head", "Soustavy rovnic", "Systems of equations"}, + {"mat2_unit3", "Kvadratické rovnice", "Quadratic equations"}, + {"mat2_unit3_sub", "Rozklad, diskriminant a dvojnásobný kořen.", "Factoring, the discriminant and a double root."}, + {"mat2_ex3_title", "Příklady", "Practice"}, + {"mat2_quiz3_head", "Kvadratické rovnice", "Quadratic equations"}, + {"mat2_unit4", "Lomené výrazy", "Rational expressions"}, + {"mat2_unit4_sub", "Krácení, definiční obor a násobení zlomků s proměnnou.", "Cancelling, the domain and multiplying algebraic fractions."}, + {"mat2_ex4_title", "Příklady", "Practice"}, + {"mat2_quiz4_head", "Lomené výrazy", "Rational expressions"}, + {"mat2_unit5", "Mocniny s racionálním exponentem", "Rational exponents"}, + {"mat2_unit5_sub", "Odmocniny, součin mocnin a záporný exponent.", "Roots, a product of powers and a negative exponent."}, + {"mat2_ex5_title", "Příklady", "Practice"}, + {"mat2_quiz5_head", "Mocniny s racionálním exponentem", "Rational exponents"}, + {"mat2_unit6", "Goniometrie pravoúhlého trojúhelníku", "Right-triangle trigonometry"}, + {"mat2_unit6_sub", "Sinus, kosinus a tangens ostrého úhlu.", "Sine, cosine and tangent of an acute angle."}, + {"mat2_ex6_title", "Příklady", "Practice"}, + {"mat2_quiz6_head", "Goniometrie pravoúhlého trojúhelníku", "Right-triangle trigonometry"}, + {"mat2_unit7", "Pythagorova a Eukleidovy věty", "Pythagoras and Euclid"}, + {"mat2_unit7_sub", "Přepona, obsah čtverce nad ní a úseky přepony.", "The hypotenuse, the square on it, and the projections on it."}, + {"mat2_ex7_title", "Příklady", "Practice"}, + {"mat2_quiz7_head", "Pythagorova a Eukleidovy věty", "Pythagoras and Euclid"}, + {"mat2_unit8", "Kružnice a Thaletova věta", "Circles and Thales"}, + {"mat2_unit8_sub", "Obvodový a středový úhel, délka oblouku a pravý úhel nad průměrem.", "An inscribed and a central angle, arc length, and the right angle in a semicircle."}, + {"mat2_ex8_title", "Příklady", "Practice"}, + {"mat2_quiz8_head", "Kružnice a Thaletova věta", "Circles and Thales"}, + {"mat_year3", "3. ročník", "Year 3"}, + {"mat3_sub", "Logaritmus, posloupnosti, pravděpodobnost a stereometrie.", "Logarithms, sequences, probability and solid geometry."}, + {"mat3_unit1", "Kvadratická funkce", "The quadratic function"}, + {"mat3_unit1_sub", "Vrchol, kořeny a hodnota paraboly.", "The vertex, the roots and a value of a parabola."}, + {"mat3_ex1_title", "Příklady", "Practice"}, + {"mat3_quiz1_head", "Kvadratická funkce", "The quadratic function"}, + {"mat3_unit2", "Exponenciální funkce a logaritmus", "Exponentials and logarithms"}, + {"mat3_unit2_sub", "Mocnina se základem a logaritmus jako její opak.", "A power and the logarithm as its inverse."}, + {"mat3_ex2_title", "Příklady", "Practice"}, + {"mat3_quiz2_head", "Exponenciální funkce a logaritmus", "Exponentials and logarithms"}, + {"mat3_unit3", "Goniometrické funkce", "Trigonometric functions"}, + {"mat3_unit3_sub", "Sinus a kosinus na jednotkové kružnici.", "Sine and cosine on the unit circle."}, + {"mat3_ex3_title", "Příklady", "Practice"}, + {"mat3_quiz3_head", "Goniometrické funkce", "Trigonometric functions"}, + {"mat3_unit4", "Sinová a kosinová věta", "The sine and cosine rules"}, + {"mat3_unit4_sub", "Obecný trojúhelník, poloměr kružnice a obsah.", "A general triangle, the circumradius and the area."}, + {"mat3_ex4_title", "Příklady", "Practice"}, + {"mat3_quiz4_head", "Sinová a kosinová věta", "The sine and cosine rules"}, + {"mat3_unit5", "Posloupnosti a řady", "Sequences and series"}, + {"mat3_unit5_sub", "Aritmetická a geometrická posloupnost a nekonečná řada.", "An arithmetic and a geometric sequence, and an infinite series."}, + {"mat3_ex5_title", "Příklady", "Practice"}, + {"mat3_quiz5_head", "Posloupnosti a řady", "Sequences and series"}, + {"mat3_unit6", "Kombinatorika", "Combinatorics"}, + {"mat3_unit6_sub", "Permutace, variace a kombinace.", "Permutations, variations and combinations."}, + {"mat3_ex6_title", "Příklady", "Practice"}, + {"mat3_quiz6_head", "Kombinatorika", "Combinatorics"}, + {"mat3_unit7", "Pravděpodobnost", "Probability"}, + {"mat3_unit7_sub", "Klasická pravděpodobnost, kostka, mince a karty.", "Classical probability with a die, a coin and cards."}, + {"mat3_ex7_title", "Příklady", "Practice"}, + {"mat3_quiz7_head", "Pravděpodobnost", "Probability"}, + {"mat3_unit8", "Stereometrie", "Solid geometry"}, + {"mat3_unit8_sub", "Krychle, válec, koule a jehlan.", "The cube, the cylinder, the sphere and a pyramid."}, + {"mat3_ex8_title", "Příklady", "Practice"}, + {"mat3_quiz8_head", "Stereometrie", "Solid geometry"}, + {"mat_year4", "4. ročník", "Year 4"}, + {"mat4_sub", "Analytická geometrie, derivace, integrál a maturitní opakování.", "Coordinate geometry, derivatives, integrals and a maturita review."}, + {"mat4_unit1", "Vektory", "Vectors"}, + {"mat4_unit1_sub", "Velikost, součet, skalární součin a koncový bod.", "Magnitude, a sum, the dot product and an endpoint."}, + {"mat4_ex1_title", "Příklady", "Practice"}, + {"mat4_quiz1_head", "Vektory", "Vectors"}, + {"mat4_unit2", "Analytická přímka", "The line in the plane"}, + {"mat4_unit2_sub", "Směrnice, úsek, parametrické vyjádření a vzdálenost od přímky.", "Slope, intercept, a parametric equation and the distance from a line."}, + {"mat4_ex2_title", "Příklady", "Practice"}, + {"mat4_quiz2_head", "Analytická přímka", "The line in the plane"}, + {"mat4_unit3", "Kuželosečky", "Conic sections"}, + {"mat4_unit3_sub", "Kružnice a elipsa v osové poloze.", "A circle and an ellipse aligned with the axes."}, + {"mat4_ex3_title", "Příklady", "Practice"}, + {"mat4_quiz3_head", "Kuželosečky", "Conic sections"}, + {"mat4_unit4", "Komplexní čísla", "Complex numbers"}, + {"mat4_unit4_sub", "Součet, součin, absolutní hodnota a mocnina i.", "A sum, a product, the modulus and a power of i."}, + {"mat4_ex4_title", "Příklady", "Practice"}, + {"mat4_quiz4_head", "Komplexní čísla", "Complex numbers"}, + {"mat4_unit5", "Derivace", "The derivative"}, + {"mat4_unit5_sub", "Derivace mocniny, tečna a derivace sinu.", "The derivative of a power, a tangent and the derivative of sine."}, + {"mat4_ex5_title", "Příklady", "Practice"}, + {"mat4_quiz5_head", "Derivace", "The derivative"}, + {"mat4_unit6", "Průběh funkce", "The shape of a function"}, + {"mat4_unit6_sub", "Stacionární body, lokální extrémy a nulová derivace.", "Stationary points, local extrema and a zero derivative."}, + {"mat4_ex6_title", "Příklady", "Practice"}, + {"mat4_quiz6_head", "Průběh funkce", "The shape of a function"}, + {"mat4_unit7", "Integrál", "The integral"}, + {"mat4_unit7_sub", "Určitý integrál mocniny jako obsah pod grafem.", "The definite integral of a power as the area under the graph."}, + {"mat4_ex7_title", "Příklady", "Practice"}, + {"mat4_quiz7_head", "Integrál", "The integral"}, + {"mat4_unit8", "Maturitní mix", "Maturita mix"}, + {"mat4_unit8_sub", "Logaritmus, kombinace, vektor, zlomek a kvadratická rovnice.", "A logarithm, a combination, a vector, a fraction and a quadratic equation."}, + {"mat4_ex8_title", "Příklady", "Practice"}, + {"mat4_quiz8_head", "Maturitní mix", "Maturita mix"}, diff --git a/src/progress.c b/src/progress.c index f597155..f84c39d 100644 --- a/src/progress.c +++ b/src/progress.c @@ -61,6 +61,7 @@ void load_progress(void) { lit_load_progress(); sci_load_progress(); fyz_load_progress(); + math_load_progress(); } /* Redraw the exercise rails of every unit that already has a page. */ diff --git a/src/search.c b/src/search.c index b5045ab..6f5f9d3 100644 --- a/src/search.c +++ b/src/search.c @@ -23,7 +23,7 @@ static GPtrArray *search_items; static gboolean subject_open(int i) { return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ - || i == ON_SUBJ || i == PHY_SUBJ || i == SCI_SUBJ; + || i == ON_SUBJ || i == MATH_SUBJ || i == PHY_SUBJ || i == SCI_SUBJ; } static const char *subject_page(int i) { @@ -37,6 +37,8 @@ static const char *subject_page(int i) { return "czechmap"; if (i == ON_SUBJ) return "onyears"; + if (i == MATH_SUBJ) + return "matyears"; if (i == PHY_SUBJ) return "fyzyears"; if (i == SCI_SUBJ) @@ -275,6 +277,47 @@ static void catalog_rebuild(void) { g_snprintf(page, sizeof(page), "lit4ex%d", i); catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); } + catalog_add_key("Matematika", "search_lesson", "matyears", FALSE, 1, "matematika"); + catalog_add_key("mat0_year", "search_lesson", "mat0map", FALSE, 1, "matematika zakladka"); + catalog_add_key("mat_year1", "search_lesson", "matmap", FALSE, 1, "matematika rocnik"); + catalog_add_key("mat_year2", "search_lesson", "mat2map", FALSE, 1, "matematika rocnik"); + catalog_add_key("mat_year3", "search_lesson", "mat3map", FALSE, 1, "matematika rocnik"); + catalog_add_key("mat_year4", "search_lesson", "mat4map", FALSE, 1, "matematika rocnik"); + for (i = 1; i <= MATH_N; i++) { + g_snprintf(key, sizeof(key), "mat0_unit%d", i); + g_snprintf(page, sizeof(page), "mat0unit%d", i); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "matematika"); + g_snprintf(page, sizeof(page), "mat0ex%d", i); + catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "priklady"); + } + for (i = 1; i <= MATH_N; i++) { + g_snprintf(key, sizeof(key), "mat_unit%d", i); + g_snprintf(page, sizeof(page), "matunit%d", i); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "matematika"); + g_snprintf(page, sizeof(page), "matex%d", i); + catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "priklady"); + } + for (i = 1; i <= MATH_N; i++) { + g_snprintf(key, sizeof(key), "mat2_unit%d", i); + g_snprintf(page, sizeof(page), "mat2unit%d", i); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "matematika"); + g_snprintf(page, sizeof(page), "mat2ex%d", i); + catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "priklady"); + } + for (i = 1; i <= MATH_N; i++) { + g_snprintf(key, sizeof(key), "mat3_unit%d", i); + g_snprintf(page, sizeof(page), "mat3unit%d", i); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "matematika"); + g_snprintf(page, sizeof(page), "mat3ex%d", i); + catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "priklady"); + } + for (i = 1; i <= MATH_N; i++) { + g_snprintf(key, sizeof(key), "mat4_unit%d", i); + g_snprintf(page, sizeof(page), "mat4unit%d", i); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "matematika"); + g_snprintf(page, sizeof(page), "mat4ex%d", i); + catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "priklady"); + } catalog_add_key("fyz_year1", "search_lesson", "fyzmap", FALSE, 1, "fyzika rocnik"); catalog_add_key("fyz_year2", "search_lesson", "fyz2map", FALSE, 1, "fyzika rocnik"); catalog_add_key("fyz_year3", "search_lesson", "fyz3map", FALSE, 1, "fyzika rocnik"); diff --git a/src/stats.c b/src/stats.c index 8dba401..2996236 100644 --- a/src/stats.c +++ b/src/stats.c @@ -79,6 +79,8 @@ void refresh_stats_ui(void) { progress_for_sci(&all); else if (s == PHY_SUBJ) progress_for_fyz(&all); + else if (s == MATH_SUBJ) + progress_for_math(&all); else progress_for_units(sub_unit_start[s], sub_unit_count[s], &all); } @@ -110,6 +112,7 @@ void refresh_stats_ui(void) { gboolean has_content = (s == NET_SUBJ) || (s == HW_SUBJ) || (s == ON_SUBJ) || (s == CZ_SUBJ) || (s == SCI_SUBJ) || (s == PHY_SUBJ) + || (s == MATH_SUBJ) || sub_unit_count[s] > 0; if (!count || !bar) @@ -128,6 +131,8 @@ void refresh_stats_ui(void) { progress_for_sci(&sp); else if (s == PHY_SUBJ) progress_for_fyz(&sp); + else if (s == MATH_SUBJ) + progress_for_math(&sp); else progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp); if (has_content) { @@ -399,6 +404,7 @@ GtkWidget *build_stats_page(void) { gboolean has_units = (s == NET_SUBJ) || (s == HW_SUBJ) || (s == CZ_SUBJ) || (s == ON_SUBJ) || (s == SCI_SUBJ) || (s == PHY_SUBJ) + || (s == MATH_SUBJ) || sub_unit_count[s] > 0; gboolean has_deutsch_units = sub_unit_count[s] > 0; diff --git a/src/subjects.c b/src/subjects.c index b324f19..1b712fb 100644 --- a/src/subjects.c +++ b/src/subjects.c @@ -5,7 +5,8 @@ /* ------------------------------------------------------------------ */ /* Index 0 (Deutsch), NET_SUBJ (networks), HW_SUBJ (hardware), CZ_SUBJ - * (Czech), ON_SUBJ (civics), PHY_SUBJ (physics) and SCI_SUBJ + * (Czech), ON_SUBJ (civics), MATH_SUBJ (mathematics), PHY_SUBJ + * (physics) and SCI_SUBJ * (natural sciences) are unlocked. */ const char *sub_keys[NUM_SUBJECTS] = { "Deutsch", @@ -155,7 +156,7 @@ void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr, static gboolean subject_is_open(int i) { return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ - || i == ON_SUBJ || i == PHY_SUBJ || i == SCI_SUBJ; + || i == ON_SUBJ || i == MATH_SUBJ || i == PHY_SUBJ || i == SCI_SUBJ; } static GtkWidget *subject_icon(int i) { @@ -178,6 +179,9 @@ static GtkWidget *subject_icon(int i) { else if (i == ON_SUBJ) gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), draw_civics_icon, NULL, NULL); + else if (i == MATH_SUBJ) + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), + draw_math_icon, NULL, NULL); else if (i == PHY_SUBJ) gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), draw_physics_icon, NULL, NULL); @@ -200,6 +204,8 @@ static const char *subject_target(int i) { return "czechmap"; if (i == ON_SUBJ) return "onyears"; + if (i == MATH_SUBJ) + return "matyears"; if (i == PHY_SUBJ) return "fyzyears"; if (i == SCI_SUBJ) diff --git a/src/theme.c b/src/theme.c index 7b59b7a..3fe2061 100644 --- a/src/theme.c +++ b/src/theme.c @@ -434,5 +434,6 @@ void apply_theme(void) { lit_rail_theme_reset(); sci_rail_theme_reset(); fyz_rail_theme_reset(); + math_rail_theme_reset(); book_rail_theme_reset(); } diff --git a/src/util.c b/src/util.c index 2f7ea24..387c909 100644 --- a/src/util.c +++ b/src/util.c @@ -36,6 +36,11 @@ char *app_progress_fyz; char *app_progress_fyz2; char *app_progress_fyz3; char *app_progress_fyz4; +char *app_progress_mat0; +char *app_progress_mat; +char *app_progress_mat2; +char *app_progress_mat3; +char *app_progress_mat4; static char *dup_dirname(const char *path) { char *tmp = g_strdup(path); @@ -188,6 +193,11 @@ static void set_progress_paths(const char *write_root) { g_free(app_progress_fyz2); g_free(app_progress_fyz3); g_free(app_progress_fyz4); + g_free(app_progress_mat0); + g_free(app_progress_mat); + g_free(app_progress_mat2); + g_free(app_progress_mat3); + g_free(app_progress_mat4); app_progress_dir = g_build_filename(write_root, PROGRESS_DIR_NAME, NULL); app_progress_u1 = g_build_filename(app_progress_dir, "unit1.conf", NULL); @@ -212,6 +222,11 @@ static void set_progress_paths(const char *write_root) { app_progress_fyz2 = g_build_filename(app_progress_dir, "fyz2.conf", NULL); app_progress_fyz3 = g_build_filename(app_progress_dir, "fyz3.conf", NULL); app_progress_fyz4 = g_build_filename(app_progress_dir, "fyz4.conf", NULL); + app_progress_mat0 = g_build_filename(app_progress_dir, "mat0.conf", NULL); + app_progress_mat = g_build_filename(app_progress_dir, "mat.conf", NULL); + app_progress_mat2 = g_build_filename(app_progress_dir, "mat2.conf", NULL); + app_progress_mat3 = g_build_filename(app_progress_dir, "mat3.conf", NULL); + app_progress_mat4 = g_build_filename(app_progress_dir, "mat4.conf", NULL); } /* Resolve read-only asset root, chdir there, and point progress at a writable