skillplay 1.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of skillplay might be problematic. Click here for more details.

Files changed (151) hide show
  1. skillplay-1.1.1/.github/workflows/ci.yml +100 -0
  2. skillplay-1.1.1/ARCHITECTURE.md +357 -0
  3. skillplay-1.1.1/PKG-INFO +13 -0
  4. skillplay-1.1.1/README.md +96 -0
  5. skillplay-1.1.1/config.sample.yaml +30 -0
  6. skillplay-1.1.1/docs/authoring-packs.md +143 -0
  7. skillplay-1.1.1/docs/editor-integration.md +76 -0
  8. skillplay-1.1.1/pyproject.toml +41 -0
  9. skillplay-1.1.1/scripts/build_binary.py +76 -0
  10. skillplay-1.1.1/skillplay/__init__.py +3 -0
  11. skillplay-1.1.1/skillplay/__main__.py +24 -0
  12. skillplay-1.1.1/skillplay/core/__init__.py +39 -0
  13. skillplay-1.1.1/skillplay/core/achievements.py +73 -0
  14. skillplay-1.1.1/skillplay/core/adaptive.py +291 -0
  15. skillplay-1.1.1/skillplay/core/capstone.py +140 -0
  16. skillplay-1.1.1/skillplay/core/cli.py +823 -0
  17. skillplay-1.1.1/skillplay/core/config.py +61 -0
  18. skillplay-1.1.1/skillplay/core/demo.py +138 -0
  19. skillplay-1.1.1/skillplay/core/engine.py +620 -0
  20. skillplay-1.1.1/skillplay/core/exam.py +122 -0
  21. skillplay-1.1.1/skillplay/core/generate.py +642 -0
  22. skillplay-1.1.1/skillplay/core/goals.py +138 -0
  23. skillplay-1.1.1/skillplay/core/hlr.py +73 -0
  24. skillplay-1.1.1/skillplay/core/i18n.py +112 -0
  25. skillplay-1.1.1/skillplay/core/leaderboard_server.py +288 -0
  26. skillplay-1.1.1/skillplay/core/loader.py +214 -0
  27. skillplay-1.1.1/skillplay/core/mentor.py +214 -0
  28. skillplay-1.1.1/skillplay/core/progress.py +184 -0
  29. skillplay-1.1.1/skillplay/core/registry.py +314 -0
  30. skillplay-1.1.1/skillplay/core/registry_index.json +8 -0
  31. skillplay-1.1.1/skillplay/core/schema.py +177 -0
  32. skillplay-1.1.1/skillplay/core/share.py +61 -0
  33. skillplay-1.1.1/skillplay/core/skillgraph.py +161 -0
  34. skillplay-1.1.1/skillplay/core/sound.py +34 -0
  35. skillplay-1.1.1/skillplay/core/stats.py +88 -0
  36. skillplay-1.1.1/skillplay/core/streak.py +33 -0
  37. skillplay-1.1.1/skillplay/core/sync.py +182 -0
  38. skillplay-1.1.1/skillplay/core/telemetry.py +66 -0
  39. skillplay-1.1.1/skillplay/core/validators.py +568 -0
  40. skillplay-1.1.1/skillplay/packs/algorithms/challenges/01-fib.yaml +36 -0
  41. skillplay-1.1.1/skillplay/packs/algorithms/challenges/02-factorial.yaml +34 -0
  42. skillplay-1.1.1/skillplay/packs/algorithms/challenges/03-prime.yaml +38 -0
  43. skillplay-1.1.1/skillplay/packs/algorithms/challenges/04-max.yaml +31 -0
  44. skillplay-1.1.1/skillplay/packs/algorithms/pack.yaml +10 -0
  45. skillplay-1.1.1/skillplay/packs/css-basics/challenges/01-p.yaml +16 -0
  46. skillplay-1.1.1/skillplay/packs/css-basics/challenges/02-id.yaml +16 -0
  47. skillplay-1.1.1/skillplay/packs/css-basics/challenges/03-class.yaml +16 -0
  48. skillplay-1.1.1/skillplay/packs/css-basics/challenges/04-desc.yaml +17 -0
  49. skillplay-1.1.1/skillplay/packs/css-basics/challenges/05-color.yaml +20 -0
  50. skillplay-1.1.1/skillplay/packs/css-basics/challenges/06-em.yaml +20 -0
  51. skillplay-1.1.1/skillplay/packs/css-basics/pack.yaml +10 -0
  52. skillplay-1.1.1/skillplay/packs/data-structures/challenges/01-first.yaml +41 -0
  53. skillplay-1.1.1/skillplay/packs/data-structures/challenges/02-last.yaml +41 -0
  54. skillplay-1.1.1/skillplay/packs/data-structures/challenges/03-reverse.yaml +41 -0
  55. skillplay-1.1.1/skillplay/packs/data-structures/challenges/04-count.yaml +41 -0
  56. skillplay-1.1.1/skillplay/packs/data-structures/pack.yaml +14 -0
  57. skillplay-1.1.1/skillplay/packs/fix-bug/challenges/01-sum.yaml +31 -0
  58. skillplay-1.1.1/skillplay/packs/fix-bug/challenges/02-greet.yaml +31 -0
  59. skillplay-1.1.1/skillplay/packs/fix-bug/challenges/03-max.yaml +31 -0
  60. skillplay-1.1.1/skillplay/packs/fix-bug/challenges/04-even.yaml +31 -0
  61. skillplay-1.1.1/skillplay/packs/fix-bug/pack.yaml +12 -0
  62. skillplay-1.1.1/skillplay/packs/fix-bug-js/challenges/01-double.yaml +33 -0
  63. skillplay-1.1.1/skillplay/packs/fix-bug-js/challenges/02-sum.yaml +36 -0
  64. skillplay-1.1.1/skillplay/packs/fix-bug-js/challenges/03-palindrome.yaml +34 -0
  65. skillplay-1.1.1/skillplay/packs/fix-bug-js/pack.yaml +12 -0
  66. skillplay-1.1.1/skillplay/packs/freeform-intro/challenges/01-greet.yaml +32 -0
  67. skillplay-1.1.1/skillplay/packs/freeform-intro/challenges/02-sum_even.yaml +34 -0
  68. skillplay-1.1.1/skillplay/packs/freeform-intro/challenges/03-palindrome-js.yaml +33 -0
  69. skillplay-1.1.1/skillplay/packs/freeform-intro/pack.yaml +12 -0
  70. skillplay-1.1.1/skillplay/packs/git-basics/challenges/01-init.yaml +19 -0
  71. skillplay-1.1.1/skillplay/packs/git-basics/challenges/02-status.yaml +20 -0
  72. skillplay-1.1.1/skillplay/packs/git-basics/challenges/03-add.yaml +20 -0
  73. skillplay-1.1.1/skillplay/packs/git-basics/challenges/04-commit.yaml +21 -0
  74. skillplay-1.1.1/skillplay/packs/git-basics/challenges/05-branch.yaml +20 -0
  75. skillplay-1.1.1/skillplay/packs/git-basics/challenges/06-mc-stage.yaml +28 -0
  76. skillplay-1.1.1/skillplay/packs/git-basics/challenges/07-mc-clone.yaml +28 -0
  77. skillplay-1.1.1/skillplay/packs/git-basics/challenges/08-mc-merge-rebase.yaml +28 -0
  78. skillplay-1.1.1/skillplay/packs/git-basics/challenges/09-mc-undo.yaml +29 -0
  79. skillplay-1.1.1/skillplay/packs/git-basics/challenges/10-mc-remote.yaml +28 -0
  80. skillplay-1.1.1/skillplay/packs/git-basics/pack.yaml +10 -0
  81. skillplay-1.1.1/skillplay/packs/http-rest/challenges/01-get.yaml +20 -0
  82. skillplay-1.1.1/skillplay/packs/http-rest/challenges/02-post.yaml +20 -0
  83. skillplay-1.1.1/skillplay/packs/http-rest/challenges/03-put.yaml +20 -0
  84. skillplay-1.1.1/skillplay/packs/http-rest/challenges/04-404.yaml +20 -0
  85. skillplay-1.1.1/skillplay/packs/http-rest/challenges/05-ctype.yaml +16 -0
  86. skillplay-1.1.1/skillplay/packs/http-rest/challenges/06-auth.yaml +21 -0
  87. skillplay-1.1.1/skillplay/packs/http-rest/pack.yaml +10 -0
  88. skillplay-1.1.1/skillplay/packs/mini-cli/challenges/01-greet.yaml +33 -0
  89. skillplay-1.1.1/skillplay/packs/mini-cli/challenges/02-add.yaml +33 -0
  90. skillplay-1.1.1/skillplay/packs/mini-cli/challenges/03-main.yaml +32 -0
  91. skillplay-1.1.1/skillplay/packs/mini-cli/pack.yaml +17 -0
  92. skillplay-1.1.1/skillplay/packs/regex-101/challenges/01-literal.yaml +20 -0
  93. skillplay-1.1.1/skillplay/packs/regex-101/challenges/02-digit.yaml +20 -0
  94. skillplay-1.1.1/skillplay/packs/regex-101/challenges/03-nondigit.yaml +20 -0
  95. skillplay-1.1.1/skillplay/packs/regex-101/challenges/04-start.yaml +20 -0
  96. skillplay-1.1.1/skillplay/packs/regex-101/challenges/05-end.yaml +20 -0
  97. skillplay-1.1.1/skillplay/packs/regex-101/challenges/06-plus.yaml +20 -0
  98. skillplay-1.1.1/skillplay/packs/regex-101/challenges/07-star.yaml +20 -0
  99. skillplay-1.1.1/skillplay/packs/regex-101/challenges/08-optional.yaml +20 -0
  100. skillplay-1.1.1/skillplay/packs/regex-101/challenges/09-class.yaml +20 -0
  101. skillplay-1.1.1/skillplay/packs/regex-101/challenges/10-negclass.yaml +20 -0
  102. skillplay-1.1.1/skillplay/packs/regex-101/challenges/11-boundary.yaml +20 -0
  103. skillplay-1.1.1/skillplay/packs/regex-101/challenges/12-alt.yaml +20 -0
  104. skillplay-1.1.1/skillplay/packs/regex-101/challenges/13-range.yaml +20 -0
  105. skillplay-1.1.1/skillplay/packs/regex-101/challenges/14-group.yaml +20 -0
  106. skillplay-1.1.1/skillplay/packs/regex-101/challenges/15-anchored.yaml +20 -0
  107. skillplay-1.1.1/skillplay/packs/regex-101/pack.yaml +10 -0
  108. skillplay-1.1.1/skillplay/packs/schemas/challenge.schema.json +80 -0
  109. skillplay-1.1.1/skillplay/packs/schemas/pack.schema.json +31 -0
  110. skillplay-1.1.1/skillplay/packs/shell-basics/challenges/01-ls.yaml +16 -0
  111. skillplay-1.1.1/skillplay/packs/shell-basics/challenges/02-pwd.yaml +16 -0
  112. skillplay-1.1.1/skillplay/packs/shell-basics/challenges/03-cd.yaml +16 -0
  113. skillplay-1.1.1/skillplay/packs/shell-basics/challenges/04-rm.yaml +16 -0
  114. skillplay-1.1.1/skillplay/packs/shell-basics/challenges/05-grep.yaml +17 -0
  115. skillplay-1.1.1/skillplay/packs/shell-basics/challenges/06-find.yaml +17 -0
  116. skillplay-1.1.1/skillplay/packs/shell-basics/pack.yaml +10 -0
  117. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/01-select.yaml +24 -0
  118. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/02-where.yaml +24 -0
  119. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/03-order.yaml +24 -0
  120. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/04-limit.yaml +24 -0
  121. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/05-count.yaml +24 -0
  122. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/06-distinct.yaml +24 -0
  123. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/07-and.yaml +24 -0
  124. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/08-or.yaml +24 -0
  125. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/09-in.yaml +24 -0
  126. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/10-between.yaml +24 -0
  127. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/11-like.yaml +24 -0
  128. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/12-join.yaml +27 -0
  129. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/13-group.yaml +27 -0
  130. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/14-having.yaml +26 -0
  131. skillplay-1.1.1/skillplay/packs/sql-basics/challenges/15-subquery.yaml +24 -0
  132. skillplay-1.1.1/skillplay/packs/sql-basics/pack.yaml +10 -0
  133. skillplay-1.1.1/skillplay/tui/__init__.py +3 -0
  134. skillplay-1.1.1/skillplay/tui/app.py +1108 -0
  135. skillplay-1.1.1/tests/conftest.py +49 -0
  136. skillplay-1.1.1/tests/leaderboard_server_test.py +115 -0
  137. skillplay-1.1.1/tests/registry_test.py +48 -0
  138. skillplay-1.1.1/tests/smoke_core.py +90 -0
  139. skillplay-1.1.1/tests/smoke_tui.py +70 -0
  140. skillplay-1.1.1/tests/test_capstone.py +82 -0
  141. skillplay-1.1.1/tests/test_community.py +82 -0
  142. skillplay-1.1.1/tests/test_exam.py +85 -0
  143. skillplay-1.1.1/tests/test_features.py +166 -0
  144. skillplay-1.1.1/tests/test_hardening.py +165 -0
  145. skillplay-1.1.1/tests/test_hlr.py +98 -0
  146. skillplay-1.1.1/tests/test_intelligence.py +428 -0
  147. skillplay-1.1.1/tests/test_mentor.py +87 -0
  148. skillplay-1.1.1/tests/test_skillgraph.py +150 -0
  149. skillplay-1.1.1/tests/test_sync.py +152 -0
  150. skillplay-1.1.1/tests/test_v9.py +80 -0
  151. skillplay-1.1.1/tests/test_validators.py +210 -0
@@ -0,0 +1,100 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.11", "3.12"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - name: Install
21
+ run: |
22
+ python -m pip install --upgrade pip
23
+ pip install -e ".[dev]"
24
+ - name: Validate packs
25
+ run: python -m skillplay validate-packs
26
+ - name: Run tests
27
+ run: python -m pytest tests/ -q
28
+
29
+ lint:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: actions/setup-python@v5
34
+ with:
35
+ python-version: "3.12"
36
+ - name: Install ruff
37
+ run: python -m pip install --upgrade pip ruff
38
+ - name: Ruff lint
39
+ run: python -m ruff check .
40
+ - name: Ruff format check
41
+ run: python -m ruff format --check .
42
+
43
+ publish:
44
+ needs: test
45
+ if: startsWith(github.ref, 'refs/tags/v')
46
+ runs-on: ubuntu-latest
47
+ # id-token: write is required for PyPI trusted publishing (OIDC) and for
48
+ # generating build attestations. No PYPI_API_TOKEN secret is needed when
49
+ # the project is configured as a trusted publisher on pypi.org.
50
+ permissions:
51
+ contents: read
52
+ id-token: write
53
+ steps:
54
+ - uses: actions/checkout@v4
55
+ - uses: actions/setup-python@v5
56
+ with:
57
+ python-version: "3.12"
58
+ - name: Build distribution
59
+ run: |
60
+ python -m pip install --upgrade pip build
61
+ python -m build
62
+ - name: Publish to PyPI
63
+ uses: pypa/gh-action-pypi-publish@release/v1
64
+ with:
65
+ password: ${{ secrets.PYPI_API_TOKEN }}
66
+ # Homebrew: publishing to a tap is repo-specific; the wheel on PyPI is the
67
+ # source of truth. Add a homebrew tap formula in a separate repo pointing
68
+ # at the PyPI release when you want `brew install skillplay`.
69
+
70
+ # D: ship a single self-contained binary per tag, for each major OS.
71
+ build-binary:
72
+ needs: test
73
+ if: startsWith(github.ref, 'refs/tags/v')
74
+ strategy:
75
+ matrix:
76
+ include:
77
+ - os: ubuntu-latest
78
+ artifact: skillplay-linux
79
+ - os: macos-latest
80
+ artifact: skillplay-macos
81
+ - os: windows-latest
82
+ artifact: skillplay-windows.exe
83
+ runs-on: ${{ matrix.os }}
84
+ permissions:
85
+ contents: read
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+ - uses: actions/setup-python@v5
89
+ with:
90
+ python-version: "3.12"
91
+ - name: Install PyInstaller
92
+ run: python -m pip install --upgrade pip pyinstaller
93
+ - name: Build single binary
94
+ run: python scripts/build_binary.py --method pyinstaller
95
+ - name: Upload binary
96
+ uses: actions/upload-artifact@v4
97
+ with:
98
+ name: ${{ matrix.artifact }}
99
+ path: dist/skillplay*
100
+ if-no-files-found: error
@@ -0,0 +1,357 @@
1
+ # skillplay — Architecture
2
+
3
+ > System design: project structure, data schemas, and runtime flows.
4
+ > Companion docs: `roadmap.md` (phases/status). This file describes **how the system works as built**.
5
+
6
+ ## 1. Tech choices
7
+
8
+ - Python 3.12, Textual (TUI) + Rich (rendering), PyYAML (packs), platformdirs (data dir), `sqlite3` stdlib (SQL validation).
9
+ - Offline-first, keyboard-only. Sound off by default (`settings.sound: false`, no audio backend wired).
10
+ - Entry point: `skillplay = "skillplay.__main__:main"` (`pyproject.toml`), `SkillPlayApp().run()`.
11
+
12
+ ## 2. Project structure
13
+
14
+ ```
15
+ skillplay/ # project root
16
+ pyproject.toml # hatchling build, deps, console script
17
+ roadmap.md # phases + verified state
18
+ ARCHITECTURE.md # this file
19
+ README.md # overview + pipx install + usage
20
+ config.sample.yaml # documented user config template
21
+ docs/
22
+ authoring-packs.md # community pack authoring guide
23
+ skillplay/ # package
24
+ __init__.py
25
+ __main__.py # main() -> TUI, or dispatches CLI subcommands
26
+ core/
27
+ __init__.py
28
+ loader.py # pack discovery + YAML parsing (loader.py:49-127)
29
+ validators.py # answer checking, behavior not strings (validators.py:16-150)
30
+ engine.py # session loop, scoring, SRS, weighting (engine.py:23-150)
31
+ progress.py # progress.json load/save, XP math (progress.py:18-58)
32
+ streak.py # daily streak rules (streak.py:9-33)
33
+ schema.py # lightweight pack/challenge schema checks
34
+ config.py # optional config.yaml merge over settings
35
+ cli.py # validate-packs / new-pack / export-stats / leaderboard
36
+ tui/
37
+ __init__.py
38
+ app.py # HomeScreen / PlayScreen / SummaryScreen / StatsScreen / SkillPlayApp
39
+ packs/
40
+ sql-basics/ (15 challenges)
41
+ regex-101/ (15 challenges, regex_tester)
42
+ git-basics/ (10 challenges, exact + multiple_choice)
43
+ fix-bug/ (4 challenges, test_cases / python)
44
+ fix-bug-js/ (3 challenges, test_cases / js via node)
45
+ css-basics/ (6 challenges, exact + multiple_choice)
46
+ shell-basics/ (6 challenges, exact)
47
+ http-rest/ (6 challenges, exact + multiple_choice)
48
+ data-structures/ (4 challenges, test_cases / python)
49
+ algorithms/ (4 challenges, test_cases / python)
50
+ schemas/
51
+ pack.schema.json
52
+ challenge.schema.json
53
+ tests/
54
+ smoke_core.py # loader/validators/engine/streak checks (isolated tmp data dir)
55
+ smoke_tui.py # headless Textual E2E: Home→Play→retry→Summary→Home
56
+ conftest.py # tmp-data-dir fixture + pack fixtures
57
+ test_validators.py # pytest: per-validator + engine unit tests
58
+ ```
59
+
60
+ Module responsibilities:
61
+
62
+ | Module | Owns | Never touches |
63
+ |---|---|---|
64
+ | `core/loader` | finding + parsing packs into `Pack`/`Challenge` dataclasses | UI, progress file |
65
+ | `core/validators` | `validate(challenge, input) -> Result(correct, detail)` | session state, disk |
66
+ | `core/engine` | challenge selection, scoring, SRS box updates, finalize, daily pick | widgets, YAML |
67
+ | `core/progress` | `progress.json` load/save, `level_for_xp` / `xp_for_level` | challenges |
68
+ | `core/streak` | pure date math for streaks | disk, UI |
69
+ | `core/schema` | `validate_pack(pack, strict) -> (errors, warnings)` — required keys, known modes, mode-specific fields, duplicate ids/prompts, over-broad regex, reference self-validation | disk, UI |
70
+ | `core/config` | optional `config.yaml` merge over `settings` | disk |
71
+ | `core/cli` | `validate-packs` / `export-stats` / `leaderboard` subcommands | UI |
72
+ | `tui/app` | screens, input, rendering, wiring engine calls | YAML parsing, SQL |
73
+
74
+ Dependency direction: `tui → core.{engine,loader,progress,streak}`; `engine → validators, progress, streak`. Validators and streak are leaf modules (no internal imports).
75
+
76
+ ## 3. Schemas
77
+
78
+ ### 3.1 Pack manifest (`pack.yaml`)
79
+
80
+ ```yaml
81
+ id: sql-basics
82
+ name: SQL Basics
83
+ version: 0.1.0
84
+ author: core
85
+ skill: sql
86
+ description: SELECT, WHERE, ORDER BY — learn SQL by doing.
87
+ difficulty: beginner
88
+ schema_version: 1 # bump on manifest-format changes
89
+ tags: [sql, beginner] # shown on Home; aids discovery
90
+ license: MIT # optional
91
+ min_version: "0.1.0" # optional: requires skillplay >= this
92
+ contributor: core # defaults to author
93
+ entry: challenges/*.yaml # glob, default challenges/*.yaml
94
+ ```
95
+
96
+ Discovery order (`loader.py:49-56`): builtin `skillplay/packs/` → `%LOCALAPPDATA%/skillplay/packs` (user) → `./packs` (local). First pack id wins (dedupe in `load_all_packs`); dirs without `pack.yaml` or with zero challenges are skipped, never fatal.
97
+
98
+ ### 3.2 Challenge file
99
+
100
+ Required: `id`, `type`. Defaults: `difficulty: 1`, `xp: 10`, `topic/title/hints/explanation` empty. `skill` is inherited from the pack manifest (`loader.py:86`). Real example (`02-where.yaml`):
101
+
102
+ ```yaml
103
+ id: sql-where-01
104
+ title: Filter with WHERE
105
+ topic: where
106
+ difficulty: 1
107
+ xp: 15
108
+ type: sql_query
109
+ prompt: |
110
+ Return the names of users who are older than 30.
111
+ context:
112
+ db_seed_sql: |
113
+ CREATE TABLE users(name TEXT, age INT);
114
+ INSERT INTO users VALUES ('ana', 25), ('bob', 35), ('cid', 40);
115
+ answer:
116
+ reference_sql: "SELECT name FROM users WHERE age > 30"
117
+ validation:
118
+ mode: sql_result
119
+ order_matters: false
120
+ normalize: true
121
+ hints: [...]
122
+ explanation: |
123
+ `WHERE` filters rows before they are projected.
124
+ ```
125
+
126
+ `type` values in use / planned: `sql_query`, `regex_build`, `git_command`, `multiple_choice` (used) → `fill_blank`, `fix_bug`, `ordering` (planned).
127
+
128
+ #### Challenge field reference (plugin API contract)
129
+
130
+ | Key | Required | Default | Notes |
131
+ |---|---|---|---|
132
+ | `id` | yes | — | unique across all packs; used for SRS + `completed_ids` |
133
+ | `type` | yes | — | selects validator family (`sql_query` today) |
134
+ | `prompt` | no | `""` | Markdown-ish text shown to the player |
135
+ | `title` / `topic` | no | `id` / `""` | `topic` groups stats later (e.g. `where`) |
136
+ | `difficulty` | no | `1` | 1–5 scale; currently informational only |
137
+ | `xp` | no | `10` | base XP before combo multiplier / retry halving |
138
+ | `answer` | no | `{}` | `reference_sql` for `sql_result`; `value`/`answers` for `exact` |
139
+ | `validation` | no | `{}` | must contain a known `mode` or validation always fails (§3.3) |
140
+ | `starter_code` | no (freeform) | `""` | V1: template pre-filled in the editor for `freeform` challenges (writing from scratch) |
141
+ | `validation.mode` | effectively | — | `exact` \| `regex_tester` \| `sql_result` \| `multiple_choice` \| `test_cases` \| `freeform` |
142
+ | `validation.normalize` | no | `true` | sort both row sets (order-insensitive compare) |
143
+ | `validation.order_matters` | no | `false` | skip sorting; use with `ORDER BY` challenges |
144
+ | `validation.case_insensitive` / `strip` | no | `false` / `true` | `exact` mode only |
145
+ | `validation.answers` | no | `[answer.value]` | accepted strings for `exact` mode |
146
+ | `validation.answer_id` | yes (MC) | — | correct option id for `multiple_choice`; must appear in `options[]` |
147
+ | `validation.must_match` / `must_not_match` | no | `[]` | test strings for `regex_tester` (empty = always fail) |
148
+ | `context.db_seed_sql` | no | `""` | `CREATE`/`INSERT` script run before both user + reference queries |
149
+ | `options[]` | yes (MC) | `[]` | `[{id, text}, …]` — rendered as buttons; max 8 (fixed button pool) |
150
+ | `hints` | no | `[]` | `hints[0]` shown on `Ctrl+H` |
151
+ | `explanation` | no | `""` | shown after a correct answer and on demand via `Ctrl+E` |
152
+ | `skill` | no (forbidden) | inherited | always overwritten from pack manifest (`loader.py:86`) — do not set per-challenge |
153
+ | `srs_reason` | no (runtime) | `""` | set by the engine on selection (`reason_for_challenge`); shown as `why: …` in PlayScreen — never authored in YAML |
154
+
155
+ ### 3.3 Validators (`validators.py:90-150`)
156
+
157
+ Dispatch on `validation.mode`:
158
+
159
+ | Mode | Rule | Used by |
160
+ |---|---|---|
161
+ | `exact` | string match after optional `strip` / `case_insensitive`; `answers[]` or `answer.value` | git-basics commands |
162
+ | `regex_tester` | compile user input; must match all `must_match`, none of `must_not_match` | regex-101 |
163
+ | `sql_result` | run user SQL + `reference_sql` on seeded in-memory sqlite, compare result sets | sql-basics |
164
+ | `multiple_choice` | compare chosen option id (case-insensitive) to `validation.answer_id` | git-basics concepts |
165
+ | `test_cases` | run user code in sandboxed subprocess; check named `function` against `test_cases` | fix-bug, data-structures, algorithms |
166
+ | `freeform` (V1) | identical runtime to `test_cases` but the TUI hides the test list and pre-fills `starter_code` so the player writes the function from scratch | freeform-intro |
167
+ | unknown | always `Result(False, "Unknown validation mode: …")` | — |
168
+
169
+ SQL safety (`validators.py:_run_sql` / `_reject_risky_sql`): seed via `executescript` first, **then** `PRAGMA query_only = ON`, then run the user query. Reversed order would block the seed itself. `normalize: true` sorts both row sets (order-insensitive); ordered challenges keep `normalize: false` (see `03-order.yaml`). Invalid SQL returns `Result(False, "SQL error: …")` — never raises.
170
+
171
+ Hardening (`validators.py`):
172
+ - `_reject_risky_sql` strips comments/string literals, then rejects multi-statement input (`;` followed by content) and write keywords (`INSERT/UPDATE/DELETE/DROP/…`) before the query runs — defense in depth on top of `query_only`.
173
+ - `_run_sql` installs `set_progress_handler` and aborts any query exceeding `_MAX_SQL_STEPS` (runaway `WITH RECURSIVE` / cartesian joins); results beyond `_MAX_SQL_ROWS` are rejected.
174
+
175
+ Never compare SQL or regex answers by string equality — only by behavior (rows returned / strings matched).
176
+
177
+ ### 3.4 Progress (`progress.json`)
178
+
179
+ Location via `platformdirs.user_data_dir("skillplay")`: `%LOCALAPPDATA%\skillplay\progress.json` (Windows), `~/.local/share/skillplay/` (Linux), `~/Library/Application Support/skillplay/` (macOS).
180
+
181
+ ```json
182
+ {
183
+ "version": 1,
184
+ "total_xp": 40,
185
+ "skills": {
186
+ "sql": { "xp": 40, "level": 1, "completed_ids": ["sql-select-01"], "attempts": 9, "correct": 5 }
187
+ },
188
+ "challenges": {
189
+ "sql-where-01": { "seen": 1, "correct": 1, "box": 2, "next_due": "2026-09-06" }
190
+ },
191
+ "streak": { "current": 1, "longest": 1, "last_played_date": "2026-09-05" },
192
+ "settings": { "session_size": 8, "sound": false }
193
+ }
194
+ ```
195
+
196
+ - XP/level (`progress.py:18-23`): `level = floor(sqrt(xp/100)) + 1`; `xp_for_level(L) = (L-1)² × 100`.
197
+ - SRS box (`engine.py:106-121`): correct → `box = min(5, box+1)`; wrong → `box = 1`. `next_due` offsets by box: `{1:0, 2:1, 3:3, 4:7, 5:16}` days.
198
+ - V3 Half-life regression (HLR): `next_due` is now driven by a per-card learned
199
+ half-life (`core/hlr.py`), not the fixed box map. Each review takes one gradient
200
+ step using the delay since `last_review`, correctness, and response `latency`
201
+ (slow correct answers shrink stability — the W4 hesitant-recall rule). The card
202
+ is rescheduled when predicted recall is expected to fall to `TARGET_RECALL`
203
+ (0.90). `box` is still updated for backward-compatible weighting/display; new
204
+ cards stay due today on first encounter. `Stats` shows per-skill avg `memory`
205
+ half-life.
206
+ - Save is atomic (`progress.py:49-54`): write `progress.tmp` + `os.replace`. Corrupt JSON falls back to defaults (`progress.py:37-46`).
207
+
208
+ ### 3.5 Streak rules (`streak.py:9-33`)
209
+
210
+ - Same day → no-op. Consecutive day (`diff == 1`) → `current += 1`. Gap (`diff > 1`) or first run → `current = 1`. `longest = max(longest, current)`. Dates stored as local ISO `YYYY-MM-DD`.
211
+
212
+ ## 4. Flows
213
+
214
+ ### 4.1 Boot (`SkillPlayApp`)
215
+
216
+ 1. `load_all_packs()` → `load()` progress (or defaults) → `apply_config` merges optional `config.yaml`.
217
+ 2. `compose` renders placeholder → `on_mount` updates streak, saves, pushes `HomeScreen`.
218
+ 3. Home shows streak / total XP (refreshed on `on_show`), plus buttons: **Stats**, **Daily Challenge**, **Mixed**, **Due today (N)**, **Goals**, **Achievements**, **Community packs**, **Settings**, and one `Button` per pack (`id="pack-{id}"`, colon-free — Textual ids forbid `:`). `Due today (N)` shows `engine.count_due_today` and launches the due-today review session (B). **Community packs** opens `CommunityScreen` (D) listing discoverable packs from the registry index with in-app Install buttons.
219
+
220
+ ### 4.2 Session loop (`PlayScreen`, `engine.py`)
221
+
222
+ ```
223
+ pick pack / daily → on_mount: Session(skill, select_challenges(pack, progress, session_size))
224
+ select: skip challenges with next_due > today; if none due, use all;
225
+ weighted sample without replacement, session_size items
226
+ weight = (6 - box) + 2×mistakes + 1 (low box & mistake-heavy first)
227
+ # A3 mistake-type targeting: add 2×dominant-type mistakes, +3 when the
228
+ # challenge's dominant type equals the player's globally weakest type
229
+ # (_weakest_mistake_type). Each chosen card gets `srs_reason`
230
+ # (reason_for_challenge) rendered as "why: …" in the TUI.
231
+ per challenge:
232
+ render HUD (Q i/n, skill XP, combo multiplier) + prompt
233
+ multiple_choice → render option buttons (fixed pool of 8, reused — ids opt0..opt7,
234
+ option ids tracked in opt_map); Input hidden.
235
+ test_cases → multi-line `TextArea` (#code, language per `validation.lang`) +
236
+ `â–¶ Run tests (Ctrl+Enter)` button; Input hidden. (A1 real code UX.)
237
+ otherwise → Input shown (up/down recalls input history)
238
+ grade via Input.Submitted or option button → engine.submit(input):
239
+ correct → combo+=1, best_combo=max, xp = int(base × mult)
240
+ mult: combo≥5 → 1.5x, ≥3 → 1.2x, else 1.0x
241
+ retried-before → xp //= 2; advance SRS box; index+=1
242
+ wrong, first time on this challenge → retried_ids.add(id), stay (retry, half XP available)
243
+ wrong, already retried → combo=0, SRS box→1, index+=1
244
+ correct: green "+N XP" + explanation; retry offered: yellow detail; final wrong: red detail
245
+ Ctrl+H → first hint; Ctrl+E → explanation of current challenge;
246
+ Ctrl+S → skip_current (advance, no grade, no SRS/XP);
247
+ Esc → _finish(): finalize (once, guarded by _finalized) → Summary. No progress lost.
248
+ all answered → _finish(): finalize() if any results → pop Play → push Summary
249
+ ```
250
+
251
+ `Session` state (`engine.py`): `index`, `combo`/`best_combo`, `correct_count`, `xp_gained`, `results[]`, `retried_ids{}`. `done` ⟺ `index >= len(challenges)`.
252
+
253
+ Daily challenge: `engine.daily_challenge(packs)` seeds `random.Random(today)` over all challenges across packs; Home wraps the single pick in a synthetic one-challenge `Pack` and pushes `PlayScreen`.
254
+
255
+ Due-today review (B): `engine.select_due_today(packs, progress, size)` returns only SRS-due (or never-seen) cards across *all* skills, sorted weakest-first; Home's `Due today` button launches it as the daily habit loop. Goals screen (B): each goal exposes a `▶ Practice` button that builds a `Session` from `engine.adaptive_order(pack, progress)` — challenges reordered weakest-first so a curated "path" self-adjusts to the player's performance rather than being a fixed list.
256
+
257
+ ### 4.3 Finalize + summary (`engine.py`, `app.py`)
258
+
259
+ `finalize`: `total_xp += session.xp_gained`; per-skill `xp`, recompute `level`, `attempts += total`, `correct += correct_count`, append correct ids to `completed_ids`; atomic save. Summary screen then updates streak + saves again, shows XP gained / accuracy / best combo; "Back to home" pops once, revealing Home.
260
+
261
+ ### 4.4 Screen stack
262
+
263
+ `[base "Loading…" | Home] → push Play → [base | Home | Play] → finish: pop Play, push Summary → [base | Home | Summary] → back: pop once → [base | Home]`. Stats likewise: push StatsScreen → pop back. The base screen is never visible after mount. StatsScreen reads progress live (total XP, streak current/longest, per-skill level/XP/accuracy/completed).
264
+
265
+ ### 4.7 Mastery exams (V6 / W6)
266
+
267
+ A real learning metric (not engagement). `core/exam.py` builds a randomized, mixed
268
+ session of ~`EXAM_SIZE` (20) challenges across all of a skill's packs, weighted
269
+ toward the *target difficulty tier* (current certification level + 1, capped at the
270
+ skill's hardest challenge). The session is graded normally; on finish,
271
+ `exam.certify` records `progress["certifications"][skill]` if accuracy ≥ 90%
272
+ (`PASS_RATIO`), setting `level` to the target tier and incrementing `exams_passed`.
273
+ Re-taking and passing raises the certified level. The TUI `ExamScreen` lists each
274
+ skill with its certified level + "Take exam" button; the result is shown on the
275
+ summary screen and persisted (with `progress.save`).
276
+
277
+ ### 4.6 Encrypted sync (V5 / W5)
278
+
279
+ Account-free, client-side-encrypted `progress.json` sync. No passwords, no
280
+ accounts — pairing is a shared **device key** (a Fernet key, `cryptography`):
281
+
282
+ 1. First device: `skillplay sync --pair` → prints the key (stored locally in
283
+ `DATA_DIR/sync.key`, *never* inside the synced blob).
284
+ 2. Other devices: `skillplay sync --set-key <KEY>` (same key).
285
+ 3. `sync --push` encrypts the on-disk `progress.json` (Fernet/AES-128-CBC+HMAC)
286
+ and `POST /sync/put` to the leaderboard server. `sync --pull` `GET /sync/get`s
287
+ the blob for an unguessable **slot** = `HMAC(key, handle)`, decrypts, and
288
+ overwrites local progress. `progress.save` snapshots the old file first, so a
289
+ pull is always undoable via `restore-progress`. The server stores only opaque
290
+ ciphertext (`leaderboard_sync.json`); it never sees the key or the plaintext.
291
+
292
+ ### 4.5 CLI (`core/cli.py`, dispatched from `__main__`)
293
+
294
+ Bare `skillplay` → TUI; with subcommands → CLI:
295
+
296
+ | Command | Behavior |
297
+ |---|---|
298
+ | `validate-packs [--json] [--strict] [--fix]` | `schema.validate_pack` on every discovered pack: required keys, known modes, mode-specific fields, duplicate ids **and prompts**, over-broad regex, and reference-answer self-validation. `--json` emits a CI-friendly report; `--strict` promotes warnings (missing explanation/hints) to errors; `--fix` prints concrete fix suggestions per problem. Exit 1 on any error. |
299
+ | `new-pack <id> [--skill S] [--dir D]` | scaffolds a new pack at `D/<id>/` with `pack.yaml` + a sample `challenges/01-example.yaml` (A2 authoring). |
300
+ | `play --pack PATH` | loads a single local pack (no install) and launches the TUI with just that pack — for authoring previews. |
301
+ | `export-stats [--format json|md] [--output PATH]` | dumps `progress.json` or a Markdown table (per-skill XP/level/accuracy/completed). |
302
+ | `leaderboard [--name] [--url] [--view] [--skill S] [--publish]` | opt-in, account-free. Default POSTs `{"name", "total_xp", "skills":{...}}` (per-skill XP; URL from flag → `SKILLPLAY_LEADERBOARD_URL` env → `leaderboard.url` config, treated as a **base** URL). `--view [--skill S]` fetches and prints the all-time + weekly boards (per skill when given). `--publish` uploads the player's progress + SVG card to the server's public `/u/<handle>` page. No URL configured → friendly exit 1. |
303
+ | `share-stats [--format md\|svg\|json] [--output PATH] [--url URL] [--publish]` | generates a Markdown snippet / SVG card / JSON dump; `--publish` posts the SVG card as a public handle page (see `leaderboard --publish`). |
304
+ | `serve-leaderboard [--host H] [--port P] [--db PATH]` | runs the opt-in, account-free leaderboard server. Stores scores in `leaderboard.json` + profiles in `leaderboard_profiles.json`. Routes: `POST /submit` (per-skill scores), `POST /profile` (public page), `GET /api/board[?skill=]`, `GET /api/profile?name=`, `GET /u/<handle>` (HTML card). Weekly board = current ISO week (rolls over); all-time is cumulative. |
305
+ | `registry [--index URL]` | D: lists discoverable community packs from the index (`core/registry.available_packs`). |
306
+ | `install <name> [--index URL] [--force]` | installs a pack by URL, local path, or index name (`install_from_index`). Updates `.registry.json` so `update-packs` can re-pull. |
307
+ | `context --file PATH [--open]` | D: editor integration — maps a file's extension to the best matching pack and (with `--open`) launches its TUI. |
308
+ | `play --pack PATH` / `play --pack-id ID` | launches the TUI with one pack (local dir, or a built-in pack by id). |
309
+
310
+ Config (`core/config.py`): optional `config.yaml` next to `progress.json`; honors `session_size`, `sound`, `leaderboard.{name,url}` — see `config.sample.yaml`.
311
+
312
+ ## 5. Invariants for contributors
313
+
314
+ 1. Validators are pure: `(challenge, str) -> Result`. No I/O, no exceptions outward.
315
+ 2. Engine mutates `progress` dict in memory; only `finalize`/`progress.save` touch disk.
316
+ 3. Never trust string equality for SQL/regex — add a `validation.mode`, not a special case.
317
+ 4. New packs = new folder under `skillplay/packs/` (or user/local dir) with `pack.yaml` + `challenges/*.yaml`. No code changes needed.
318
+ 5. Textual constraints learned the hard way: no screen ops inside `compose()`; no `self.app` access in `Screen.__init__` (use `on_mount`); widget ids `[A-Za-z0-9_-]` only; `align`/`content-align` need both axes in current Textual.
319
+
320
+ ## 6. Decision log
321
+
322
+ | Decision | Chosen | Rejected | Why |
323
+ |---|---|---|---|
324
+ | TUI framework | Textual | BubbleTea (Go), Ratatui (Rust) | content iteration speed matters more than binary size at MVP; Python lets pack authors read validator code; `run_test()` pilot gives free headless E2E |
325
+ | Pack format | YAML | TOML, JSON | multi-line `prompt`/`db_seed_sql` blocks are readable in YAML; authors are humans, not machines |
326
+ | Progress store | single JSON file, atomic replace | SQLite | progress is one small document, always read/written whole; JSON is inspectable/debuggable by users; atomic replace is enough crash safety |
327
+ | SQL validation | in-memory sqlite, result-set compare | string match, hosted DB | zero deps (stdlib), true semantic checking, per-query DB so challenges can't interfere; `query_only` after seed blocks writes (verified) |
328
+ | Challenge identity | global string `id` | per-pack numbering | SRS records, `completed_ids`, and `retried_ids` all key on it across sessions |
329
+ | Level curve | `floor(sqrt(xp/100))+1` | linear | diminishing returns keep early game rewarding without runaway numbers |
330
+
331
+ ## 7. Failure modes & limits (verified)
332
+
333
+ | # | Exposure | Behavior today | Status |
334
+ |---|---|---|---|
335
+ | 1 | Runaway user SQL (`WITH RECURSIVE` bomb, cartesian join) | `set_progress_handler` aborts after `_MAX_SQL_STEPS` (2M) instructions → `Result(False, "SQL error: interrupted")` | HANDLED (P4 guard) |
336
+ | 2 | Regex catastrophic backtracking (`(a+)+$` on long non-match) | hangs inside `_regex_tester` `.search()`; test strings are short so impact is a brief freeze | OPEN — acceptable until user-authored packs grow; then add `regex` timeout or length cap |
337
+ | 3 | `Esc` mid-session | `_finish()` → finalize (guarded once) → Summary. Answered XP + SRS updates are saved, nothing lost | HANDLED (was: silent loss — fixed in P4) |
338
+ | 4 | Multi-statement input (`SELECT …; DELETE …`) | `_reject_risky_sql` strips comments/literals, then rejects `;` followed by content before sqlite sees it → `Result(False, "SQL error: Only a single SQL statement is allowed")` | HANDLED (explicit pre-flight reject) |
339
+ | 5 | Write attempt (`DROP TABLE`, `INSERT`) | double-guarded: `_reject_risky_sql` rejects write keywords; `query_only` pragma → `OperationalError` if anything slips through | HANDLED |
340
+ | 6 | Corrupt `progress.json` | falls back to defaults (`progress.py`); corrupt file is overwritten on next save (data loss of old progress, app survives) | HANDLED (survival over recovery — by design) |
341
+ | 7 | Unknown `validation.mode` / bad pack YAML | challenge always grades wrong (`_unknown`); `validate-packs` CLI now surfaces schema errors + failed self-validation explicitly | HANDLED |
342
+ | 8 | Unbounded `fetchall` | queries returning more than `_MAX_SQL_ROWS` (5000) rows are rejected with a clean message | HANDLED (P4 row cap) |
343
+ | 9 | Option-button remount id collisions | option buttons are a fixed pool (`opt0..opt7`) reused via `opt_map`, never re-mounted per challenge | HANDLED (Textual `remove_children` is async — dynamic remount races) |
344
+
345
+ ## 8. Testing
346
+
347
+ ```
348
+ cd skillplay
349
+ python tests/smoke_core.py # 7 checks: loader, validators, retry/half-XP, finalize, streak, SRS
350
+ python tests/smoke_tui.py # headless pilot: Home→Play→wrong→retry→correct→Summary→Home
351
+ python -m pytest tests/ # unit suite: validators, engine, pack validation (needs `pytest`)
352
+ ```
353
+
354
+ - All scripts resolve the repo root from `__file__`, so they run from any cwd (packs load via builtin path).
355
+ - **Isolation:** all monkeypatch `progress.DATA_DIR`/`PROGRESS_PATH` to a fresh `tempfile.mkdtemp()` before running — real user progress is never touched. (Earlier versions wrote to the live data dir; fixed when moving into the repo.)
356
+ - `smoke_tui.py` and the pytest suite need `textual`; `smoke_core.py` needs only `pyyaml` + `platformdirs`. Dev deps: `pip install -e ".[dev]"`.
357
+ - pytest suite (`tests/test_validators.py` + `tests/conftest.py` fixtures) covers per-validator behavior (multi-statement/write rejection, runaway-query abort, multiple-choice, regex), engine retry/half-XP, session weighting, and `validate_pack` linting. Smoke scripts are retained for a zero-dep sanity check.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.5
2
+ Name: skillplay
3
+ Version: 1.1.1
4
+ Summary: Learn-by-Play TUI — learn dev skills by playing bite-sized challenges
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: cryptography>=42.0
7
+ Requires-Dist: platformdirs>=4.2
8
+ Requires-Dist: pyyaml>=6.0
9
+ Requires-Dist: rich>=13.7
10
+ Requires-Dist: textual>=0.83
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == 'dev'
13
+ Requires-Dist: ruff>=0.6; extra == 'dev'
@@ -0,0 +1,96 @@
1
+ # skillplay
2
+
3
+ > Learn-by-Play TUI: learn dev skills (SQL, regex, git, CSS, JS) through
4
+ > bite-sized terminal challenges. Offline-first, keyboard-only, zero accounts.
5
+
6
+ ## Install
7
+
8
+ With [pipx](https://pipx.pypa.io/) (recommended — isolates the app in its own env):
9
+
10
+ ```bash
11
+ pipx install skillplay # from PyPI
12
+ # or, from a clone of this repo:
13
+ pipx install . # editable install from source
14
+ ```
15
+
16
+ Or with plain pip:
17
+
18
+ ```bash
19
+ pip install . # from the repo root
20
+ python -m skillplay # launch
21
+ ```
22
+
23
+ Requires Python 3.11+.
24
+
25
+ ## Run
26
+
27
+ ```bash
28
+ skillplay # launches the TUI
29
+ skillplay validate-packs # check all installed packs against the schema
30
+ skillplay export-stats # print progress as Markdown
31
+ skillplay export-stats --format json --output stats.json
32
+ skillplay share-stats --format md # a copy-pasteable stats snippet
33
+ skillplay share-stats --format svg --output card.svg
34
+ skillplay leaderboard --name you # opt-in POST of (per-skill) XP (needs a URL)
35
+ skillplay leaderboard --view --skill sql # fetch + print the board, by skill
36
+ skillplay leaderboard --publish # publish your public profile/handle page
37
+ skillplay serve-leaderboard # run the opt-in leaderboard server (C)
38
+ skillplay registry # list discoverable community packs
39
+ skillplay install css-basics # install a community pack by index name
40
+ skillplay context --file app.py # D: suggest a pack for the file you're editing
41
+ skillplay play --pack-id sql-basics # jump straight into one built-in pack
42
+ skillplay install <url|path|name> # install a community pack (P10)
43
+ skillplay update-packs # re-install community packs
44
+ skillplay restore-progress # W5: list/restore automatic daily progress backups
45
+ skillplay sync --pair # V5: generate + print the device pairing key
46
+ skillplay sync --set-key <KEY> # V5: install a key copied from another device
47
+ skillplay sync --push # V5: encrypt + upload progress to the server
48
+ skillplay sync --pull # V5: download + decrypt progress onto this device
49
+ skillplay exam --skill sql # V6: take a randomized mastery exam (TUI)
50
+ ```
51
+
52
+ ## What's included
53
+
54
+ | Pack | Skill | Challenges | Validator |
55
+ |---|---|---|---|
56
+ | `sql-basics` | sql | 15 | `sql_result` (in-memory sqlite) |
57
+ | `regex-101` | regex | 15 | `regex_tester` |
58
+ | `git-basics` | git | 10 | `exact` + `multiple_choice` |
59
+ | `fix-bug` | python | 4 | `test_cases` (sandboxed subprocess) |
60
+ | `fix-bug-js` | javascript | 3 | `test_cases` (node, sandboxed) |
61
+ | `css-basics` | css | 6 | `exact` + `multiple_choice` |
62
+ | `shell-basics` | shell | 6 | `exact` |
63
+ | `http-rest` | http | 6 | `exact` + `multiple_choice` |
64
+ | `data-structures` | python | 4 | `test_cases` |
65
+ | `freeform-intro` | python | 3 | `freeform` (hidden-test construction) |
66
+
67
+ ## How to play
68
+
69
+ - Pick a pack (or **Mixed** for an interleaved session across all skills).
70
+ - `Ctrl+H` cycles through hints, `Ctrl+S` skips, `Ctrl+E` shows the explanation,
71
+ `Ctrl+U`/`Ctrl+D` rate the explanation, `Esc` finishes and saves the session.
72
+ - Correct answers earn XP with a combo multiplier; the first wrong answer
73
+ offers a retry for half XP.
74
+ - A Leitner spaced-repetition schedule brings weak challenges back sooner.
75
+ Mistakes are tagged (syntax / logic / off-by-one) and bias future weighting.
76
+ - Adaptive difficulty tilts selection toward harder challenges as you improve.
77
+ - Daily challenge, achievements, curated goals, and a crash-safe session
78
+ resume are all built in. Settings (sound, theme, language, telemetry) live in
79
+ the in-app **Settings** screen.
80
+ - **Mastery exams** (V6): take a randomized, mixed exam per skill — score ≥ 90%
81
+ to certify the next difficulty level. A real learning metric, not just XP.
82
+
83
+ ## Authoring your own packs
84
+
85
+ See [docs/authoring-packs.md](docs/authoring-packs.md). Packs are just a
86
+ folder of YAML — no code changes needed. The built-in `git-basics` pack is a
87
+ minimal example using only `exact` and `multiple_choice` validators.
88
+
89
+ ## Distribution
90
+
91
+ A single-binary build is supported via `scripts/build_binary.py` (PyInstaller,
92
+ falling back to `shiv`). On each tagged release (`v*`), CI builds a one-file
93
+ binary for Linux, macOS, and Windows and uploads them as artifacts
94
+ (`.github/workflows/ci.yml` → `build-binary` job), and publishes the wheel to
95
+ PyPI (`.github/workflows/ci.yml` → `publish` job).
96
+
@@ -0,0 +1,30 @@
1
+ # Sample user config
2
+ #
3
+ # Copy to the skillplay data dir (next to progress.json) and rename to
4
+ # `config.yaml`. All keys are optional; defaults are shown below.
5
+ #
6
+ # Linux/macOS: ~/.local/share/skillplay/config.yaml
7
+ # Windows: %LOCALAPPDATA%\skillplay\config.yaml
8
+
9
+ # How many challenges per session (default 8).
10
+ session_size: 8
11
+
12
+ # Sound effects (default off; dependency-free beep via terminal bell / winsound).
13
+ sound: false
14
+
15
+ # Theme: dark | light | high_contrast (respects NO_COLOR if set in the env).
16
+ theme: dark
17
+
18
+ # Language: en | es (community packs may add more).
19
+ language: en
20
+
21
+ # Anonymous telemetry of low first-try rates. Local-only by default; set true to
22
+ # also POST an anonymized aggregate (no names) to the leaderboard URL.
23
+ telemetry: false
24
+
25
+ # Opt-in online leaderboard (disabled until `url` is set — see `skillplay leaderboard --help`).
26
+ # `url` is the BASE url of a `serve-leaderboard` instance (e.g. http://localhost:8000);
27
+ # endpoints (/submit, /profile, /api/board, /u/<handle>) are appended automatically.
28
+ leaderboard:
29
+ name: anon
30
+ # url: http://localhost:8000