Challenge format: folder, stages, questions and tests

A challenge is a repository folder with a challenge.md file, stages in stages/, questions and tests in checks/ and starting files in starter/. All of it is ordinary markdown and code — written by hand or edited after generation.

Updated Copy as Markdown
In this article

The folder#

A challenge is any repository folder that contains challenge.md. A repository can have any number of challenges, and the repository root is a folder too. Challenges are read from the default branch.

parser/
  challenge.md              title, description, level, tags
  stages/
    01-why.md               a stage without checks — theory
    02-questions.md         a stage with questions
    03-code.md              a stage with code and tests
  checks/                   answers and tests
    02-questions.md         questions of stage 02
    03-code/test_parser.py  tests of stage 03
  starter/                  what the attempt repository starts with
    parser.py
  • stages/ — the stages. Their order is the order of the file names, so names start with a number with a leading zero: 01-…, 02-…, 10-…. The name without .md is the stage address: /{owner}/{repository}/learn/{folder}/stages/02-questions. A challenge at the repository root is written as - in the address.
  • checks/ — question files with marked answers, and tests. They are not shown on the stage screen and are not copied to the attempt repository. But they are ordinary repository files: in a public repository anyone can open them — like the answers at the back of a textbook.
  • starter/ — the files the attempt repository starts with. Needed only if the challenge has stages with file checks.

A folder with challenge.md inside another challenge's starter/ or checks/ is part of that challenge's content, not a separate challenge.

challenge.md#

The header is flat — only strings and lists, no nested objects:

field what it holds
title the title — required
summary one line for the list of challenges
language the language of the texts: ru, en
level beginner, intermediate or advanced
tags a list [git, basics] or a single word
source the path to the material the challenge was made from, relative to the challenge folder: ../git.md. The file must exist; the challenge page shows a «The material it was made from» link

The body is the introduction: why this challenge and what you will have at the end. The challenge page shows it.

A stage#

A stage is a stages/NN-name.md file: a header and the text of the task or theory in markdown. Three header fields describe the stage:

field what it holds
title the stage title — required
summary a short description
minutes how many minutes it takes, as a whole number — «5 min» in the list of stages

Every other header key is a check. An unknown key is a format error.

key how to write it the stage counts when
quiz quiz: checks/02-questions.md every question of the file has been answered correctly; wrong ones come back at the end of the round
self_check self_check: [item, item] or a list of - item lines all items are ticked — you check yourself
files files: [notes.md, src/parser.py] the files exist and are not empty
contains contains: and lines path: text or path: ["text", "text"] the file contains every text, case-insensitive
min_words min_words: and lines path: 150 the file has at least that many words; the file's header does not count
numeric numeric: and lines path: answer.md, answer: 42, tolerance: 0.5 the last number in the file matches the answer within the tolerance; the default tolerance is 0, comma and dot are equal
tests tests: checks/03-code/test_parser.py Python (.py) or JavaScript (.js) tests passed in the sandbox

How it adds up:

  • A stage without checks is theory. The Read, next button counts it.
  • Checks combine. A stage counts when all its parts pass: questions, self-check and files. On the stage screen this is the «What is checked» list.
  • Two kinds of paths. quiz and tests point at challenge files — relative to its folder, and the file must exist. files, contains, min_words and numeric point at files in the attempt repository — relative to its root. A path cannot leave the root through ...
  • File checks (files, contains, min_words, numeric, tests) read the attempt repository at its latest commit. A challenge with such stages gets an attempt repository at start, see Challenges.
  • The stage kind in the list comes from its checks: «Task» — there are file checks, otherwise «Questions» — there is a quiz, otherwise «Self-check», otherwise «Theory».

A stage header with a file check, like notes taken with the Cornell method:

---
title: Write your notes
files: [notes.md]
contains:
  notes.md: ["## Notes", "## Questions", "## Summary"]
min_words:
  notes.md: 150
---

A calculation with a numeric answer, and a self-check:

---
title: How long did it take
files: [speed.md]
numeric:
  path: speed.md
  answer: 250
  tolerance: 1
---
---
title: Check your notes yourself
self_check:
  - Every notes block holds a thought in your own words, not a quote
  - The summary fits into five sentences and names the main point
---

The question file#

A stage's questions live in a separate markdown file in checks/, and the stage header refers to it: quiz: checks/02-questions.md. The file reads like an ordinary note in any editor.

  • A question is a second-level heading ## …. A first-level heading and the text before the first question are an introduction, not a question.
  • The heading ends with an id in curly braces: ## What is HEAD? {#head}. An id is lowercase Latin letters, digits, - and _, up to 64 characters, unique across the whole challenge. The attempt's progress and the review schedule live by the id: you can edit the question text, but changing the id starts the question from zero.
  • The question kind is recognized by the shape of the answer. It can be named explicitly with a class next to the id: {#head .card}.
  • The body is read from the end. The last > … lines are the explanation shown after answering. Before them is the answer block. Everything above is the question text, so a numbered list or code in the question is not confused with the answer. One answer block holds one kind of lines: options, order, pairs or = … answers.
kind how to write it what the user does
choice (single) options - [ ] text, one right one - [x] text picks one option
several answers (multiple) several - [x] marked, or the .multiple class ticks all that apply; part of the right ones gives «Partially correct»
order (order) a numbered 1. list in the right order arranges the shuffled items
pairs (pairs) lines - left => right, at least two matches them; the right column is shuffled
gaps (blanks) ___ in the question text and one = answer line per gap fills in the words
number (numeric) = 42 or = 2.5 ± 0.1 (+- works too) enters a number; comma or dot
short answer (text) = answer | another variant types an answer; case, ё/е, quotes and a final period do not matter
card (card) the .card class, the back side as text under the heading recalls, opens the answer and rates themselves

Options need at least two, and at least one marked right; an order needs at least two items; a card has no answer block, only the text of its back side.

Tests#

A code stage refers to a test file in checks/, and the files the work starts with go into starter/ and are listed in the header under files.

  • Tests are an ordinary Python or JavaScript script; the language comes from the extension. The stage counts if the script runs to the end without an error: assert with a message is handy. Exiting before the end of the file — for example, sys.exit(0) in the solution file — does not count.
  • Attempt files are imported by name (from wordcount import count_words) or by path (from src.parser import parse). Attempt files of the same language go into the sandbox.
  • The run happens in a sandbox with a time limit: an infinite loop is a failure, not a hang. The test output is shown under the task.

How format errors are shown#

A challenge with errors cannot be started. The repository's Learn tab shows how many errors the challenge files have, and the challenge page lists them: file, line and what is wrong. For example:

parser/stages/03-code.md: the stage header has no `title`
parser/stages/03-code.md: unknown check `test`; available: quiz, self_check, files, contains, min_words, numeric, tests
parser/stages/02-questions.md: `quiz` refers to `checks/02.md`, and the challenge has no such file
parser/checks/02-questions.md:7: the question «What is HEAD?» has no id — add `{#short-id}` to the end of the heading
parser/checks/02-questions.md:15: question `merge-blank`: the text has 2 `___` gaps but 1 `= …` answer line

Fix the file and commit — the list updates. Attempts already started are not affected: each is pinned to the version of the challenge it started with.

Example: a challenge of questions#

A challenge about git: two theory stages, two question stages and cards. It has no attempt repository — only progress.

git-basics/challenge.md:

---
title: "Git: commit, branch, merge"
summary: How git keeps history and why a branch is just a pointer
language: en
level: beginner
tags: [git, basics]
---

Git is not a «folder of copies» but a history of snapshots, where every snapshot knows its parent. Once that is clear, the other commands stop being scary: `commit` adds a snapshot, `branch` creates one more pointer, `merge` joins two lines of history into one.

In this challenge you will work through three ideas — snapshot, branch and merge — and reinforce them with questions. The questions will come back later in «Train» mode so you do not forget them.

git-basics/stages/01-snapshots.md — theory, no checks:

---
title: History is a chain of snapshots
minutes: 5
---

## Working copy, index, history

Every repository has three places where a file lives:

1. **the working copy** — the files you edit in your editor;
2. **the index** — a draft of the next snapshot: what you marked with `git add` goes here;
3. **history** — the saved snapshots (commits).

`git commit` takes the whole index and saves it as a new snapshot. A snapshot has an author, a time, a message and a link to its **parent** — the previous snapshot. That is how the chain forms.

```text
A ← B ← C      (C knows about B, B about A)
```

## A hash is the name of a snapshot

Every commit is named by a hash of its content, for example `3f2a9c1`. Change even one letter in a file or the message and you get a different hash. That is why history cannot be swapped unnoticed: a swap changes the names of all the following snapshots.

git-basics/stages/02-commit.md — a question stage:

---
title: Check yourself — snapshots
quiz: checks/02-commit.md
---

Five questions about the working copy, the index and history. Wrong answers come back at the end of the round — the stage counts once you answer each one correctly.

git-basics/checks/02-commit.md — single choice, multiple choice and order:

---
title: Snapshots
---

## Which command puts a change into the index? {#add-to-index}

- [ ] `git commit`
- [x] `git add`
- [ ] `git push`
- [ ] `git status`

> The index is a draft of the next snapshot. `git add` fills it, and
> `git commit` saves it whole.

## What does `git commit` save? {#commit-saves}

- [ ] All files of the working copy, even unmarked ones
- [x] The content of the index
- [ ] Only the changed lines of the last file

> A commit takes the whole index: whatever was not marked with `git add` does not get into the snapshot.

## Put the path of a change from the editor to history in order {#path-of-change}

1. You edit a file in the working copy
2. `git add` — the change goes into the index
3. `git commit` — the index becomes a snapshot

## What does every commit know besides its files? {#commit-knows}

- [x] Its parent
- [x] Its author and time
- [ ] All its descendants
- [x] Its message

> A commit points back, to its parent. It does not know its descendants — they appear later.

## Why can an old commit not be swapped unnoticed? {#hash-chain}

- [ ] Git forbids editing files after a commit
- [x] The hash depends on the content, and a swap changes the names of all following snapshots
- [ ] Commits are stored on the server, not on your machine

> A hash is a fingerprint of the content and the parent. Change an old snapshot and the hashes of the whole chain after it change.

git-basics/stages/03-branches.md — the second theory stage:

---
title: A branch is a pointer
minutes: 5
---

## A branch copies nothing

A branch in git is **a file with one hash**: it points at the last commit of its line. Creating a branch means writing one more pointer, so it is instant whatever the size of the project.

`HEAD` is a pointer to where you are now: usually to a branch. A new commit moves forward the branch `HEAD` points at.

```text
      main
       ↓
A ← B ← C
         ↖
          D ← E
              ↑
            feature  ← HEAD
```

## Merging

`git merge feature`, standing on `main`, joins two lines. If `main` has not moved in the meantime, git simply moves the pointer forward (**fast-forward**). If both moved, a **merge commit** with two parents appears. A conflict happens only when both lines edited the same lines.

git-basics/stages/04-branch-questions.md:

---
title: Check yourself — branches and merging
quiz: checks/04-branches.md
---

Questions about pointers, `HEAD` and merging.

git-basics/checks/04-branches.md — pairs, a number, a short answer and a gap:

---
title: Branches
---

## What is inside a git branch? {#branch-is}

- [ ] A copy of all project files
- [x] The hash of the last commit of its line
- [ ] A list of all the branch's commits

> A branch is a pointer. That is why it is created instantly and copies nothing.

## Match the term and its meaning {#pointer-pairs}

- branch => a pointer to the last commit of a line
- HEAD => where you are now
- merge commit => a snapshot with two parents

## How many parents does a merge commit of two branches have? {#merge-parents}

= 2

> A merge joins two lines of history, so such a snapshot has two parents.

## What is a merge called when git just moves the pointer forward? {#ff-name}

= fast-forward | fast forward

> It happens when the target branch has not moved in the meantime.

## The command `git ___ feature` on the `main` branch joins the `feature` line into `main` {#merge-blank}

= merge

## When does a merge conflict happen? {#conflict}

- [ ] Always, if the branches have a different number of commits
- [x] When both lines edited the same lines
- [ ] When the branches have different names

> Git joins different files and different places in one file by itself.

git-basics/stages/05-cards.md and git-basics/checks/05-cards.md — cards:

---
title: Review cards
quiz: checks/05-cards.md
---

Four cards: recall the answer, open the back side and honestly rate whether you remembered. The rating decides when the card comes back in «Train».
---
title: Cards
---

## What is the index in git? {#card-index .card}

A draft of the next snapshot: `git add` puts the changes that will go into the commit here.

## What is HEAD? {#card-head .card}

A pointer to where you are now — usually to the current branch.

## What is a fast-forward? {#card-ff .card}

A merge without a new commit: the target branch did not move, and git just moves its pointer forward.

## How is a commit different from saving a file? {#card-commit .card}

A commit is a snapshot of the whole index with an author, a time, a message and a link to its parent; its name is the hash of its content.

Example: a stage with code and tests#

The «Word counter in Python» challenge: the user writes wordcount.py in their attempt repository, and tests check each stage.

python-wordcount/
  challenge.md
  stages/
    01-task.md             theory: what counts as a word
    02-count.md            files + tests
    03-top.md              files + tests
    04-questions.md        quiz
  checks/
    02-count/test_count.py
    03-top/test_top.py
    04-questions.md
  starter/
    wordcount.py

python-wordcount/stages/02-count.md:

---
title: Count the words
files: [wordcount.py]
tests: checks/02-count/test_count.py
---

Write `count_words(text)`: how many words the text has, by the rule from the first stage.

```python
count_words("Cat, cat and CAT!")  # 4
count_words("")                   # 0
count_words("  \n\t ")            # 0
```

Make a change and press «Check» — or `git push`, and the check starts by itself. If a test fails, its output is shown below.

python-wordcount/checks/02-count/test_count.py:

from wordcount import count_words

assert count_words("Cat, cat and CAT!") == 4, "«Cat, cat and CAT!» is four words"
assert count_words("") == 0, "an empty string has no words"
assert count_words("  \n\t ") == 0, "only whitespace is zero words"
assert count_words("one-two three") == 3, "a hyphen is a separator"
assert count_words("Python 3 came out in 2008") == 6, "numbers are words too"
print("count_words: all checks passed")

python-wordcount/starter/wordcount.py — the attempt repository starts with this file:

"""Word frequency analysis.

The functions appear stage by stage — start with `count_words`.
"""


def count_words(text):
    """How many words the text has."""
    raise NotImplementedError


def top_words(text, n):
    """The n most frequent words: a list of (word, count) pairs."""
    raise NotImplementedError

On the starter the test fails with NotImplementedError, so the stage does not count until the function is written. It is handy to start your own challenge from a copy of these files: create a folder in your repository, put challenge.md and stages/ into it, commit and open the Learn tab.

Was this helpful?