Persisting and Sharing Data

Files and Games


Learning Objectives

  • You can read game content and recorded attempts from separate CSV files.
  • You can append a new attempt without losing earlier history.
  • You can calculate statistics from checked records.
  • You can test file functions separately from a supplied graphical interface.

Word Practice asks for translations between Finnish and English. Its questions come from a vocabulary file, and its history remembers the learner’s answers. The files have visible effects: adding a word creates another question, and recording an answer changes the statistics.

Download Word Practice and work in its own folder in your IDE. The Pyxel interface is supplied. You complete ordinary functions in word_files.py, using the imports and file operations from this part, then submit through the IDE plugin or as a ZIP.

Two Files with Different Responsibilities

The vocabulary file describes the current questions:

finnish,english
yksi,one
kaksi,two
kolme,three

In Finnish-to-English mode, the game asks What is ‘yksi’ in English? In the other direction, it asks What is ‘one’ in Finnish?

An answer produces one history row:

direction,source,target,response,correct
fi-en,yksi,one,one,1
fi-en,kaksi,two,thu,0
fi-en,kolme,three,tree,0
en-fi,one,yksi,yksi,1

source is the displayed word, target is the expected translation, and response is what the learner typed. The direction keeps the two kinds of practice distinct. correct is written as 0 or 1 and converted to an integer when loaded.

These are flat records, so CSV suits both files. The JSON chapter compared this design with linked CSV tables and nested documents. Here a history row keeps its own question and expected answer, so removing a word from today’s vocabulary does not erase the meaning of an earlier attempt.

Keep the Answer Rule Explicit

The supplied answer_matches(response, target) ignores capitalization and surrounding whitespace. It preserves accents: "YÖ" matches "yö", while "yo" does not.

For a matching answer, the game displays Correct! Otherwise it displays Incorrect, the correct translation is ’…’. The recorded response keeps its original text.

Each vocabulary word has one translation in either direction for this exercise. The loader rejects repeated Finnish or English words after trimming and ignoring case. Supporting several accepted translations would require a richer record contract; it is not inferred from duplicate rows.

Record One Attempt at a Time

Follow the data through one question:

vocabulary.csv → checked word pairs → question
typed response → checked attempt → append to history.csv
history records → summary → statistics and review suggestions

Loading vocabulary and history happens at startup. Appending happens once when an answer is submitted. Drawing the next frame does not append another row.

A missing history file means there have been no saved attempts yet. A malformed existing history means loading failed. Do not silently replace that history with an empty list.

For a new history file, write the header once. For an existing valid file, append only the new row. Validate both the new attempt and any existing history before opening for append. The supplied validator checks the record fields and whether the correctness flag agrees with the recorded answer.

A manually edited CSV file may have no newline at its end. Before appending, check whether valid existing text ends with "\n" or "\r"; add a separator if neither is present, so the new answer begins on its own row.

Calculate Before Drawing

Statistics do not need Pyxel or an open file. This example starts with already-checked records. Predict the totals before running it:

No files opened Select a file to edit

Change history to [] and adjust the assertions. The counts should all be zero. The interface must also avoid dividing by zero when displaying an accuracy percentage.

The exercise extends this calculation with per-question groups. Keep the direction and both words together when deciding which group an attempt belongs to. Practising yksi → one and one → yksi can produce different results.

Test Before Opening the Game

Download the exercise with the IDE plugin or Download starter code below. Open the downloaded folder in your IDE. The supplied files have these roles:

FileResponsibility
word_files.pyYour vocabulary reader, history reader, attempt writer, and summary function
word_rules.pySupplied answer comparison and record validation
main.pySupplied Pyxel interface
vocabulary.csvEditable word pairs
browser_history.pySupplied history-status and export support; local runs use history.csv directly
6x10.bdfSupplied font for text, including Finnish letters

The game uses the supplied Misc Fixed 6×10 font, which is in the public domain. The 6x10.bdf file contains its Latin-1 characters, including ä, ö, Ä, and Ö, and retains its public-domain notice. Keep it beside main.py; the interface loads it automatically.

No history file is supplied. Your reader treats a missing history.csv as an empty history, and your writer creates it when the first answer is saved.

The handout divides the 70 points into four steps: vocabulary loading (15), history loading (20), appending (20), and statistics with the complete game (15). Submit after each step to receive its grading feedback.

Write your own checks for each function in word_files.py. Create check.py beside it, import the function you are working on, and assert the results you predict. Save both files, then run py check.py on Windows or python3 check.py on macOS/Linux from the exercise folder. The handout gives a first example. Later functions can remain unfinished while you test the current one.

Use separate test files, such as test_vocabulary.csv and test_history.csv, to keep your learning history intact. Include:

  • a quoted comma and Finnish letters;
  • two consecutive appends, retaining both records and only one header;
  • a missing history, a header-only history, and a malformed existing history;
  • an invalid attempt that must not create or change a file;
  • both translation directions and an empty summary;
  • a removed vocabulary word whose historical attempts remain readable.

A failed assertion shows which expectation was not met. Compare it with the function’s contract, fix the cause, and rerun your checks before submitting.

The handout states the exact return shapes and validation rules.

Vocabulary practice and history

0 / 70 points

Build Word Practice in four steps by completing the marked functions in word_files.py. The Pyxel interface, answer rules, record validators, review mode, and history support are supplied. Keep their behavior unchanged.

StepWorkPoints
1Load vocabulary15
2Load earlier attempts20
3Append a new attempt20
4Summarize history and complete the game15
Total70

Each step earns its points when all of that step’s grading checks pass. You can submit after each step; later unfinished steps do not prevent credit for completed earlier steps. Step 3 uses Step 2’s reader. The final step also checks that all four functions work through the supplied game.

Work locally

Download this exercise with the Aalto OpenCS IDE plugin, or use Download starter code below and extract the ZIP into its own folder inside programming-practice. Open that exercise folder in your IDE. Keep each exercise in a separate folder.

FileRole
word_files.pyImplement the four marked file and statistics functions here.
main.pySupplied Pyxel interface; run it to play after completing the functions.
word_rules.pySupplied answer comparison, validation, and review rules; keep unchanged.
browser_history.pySupplied history-status and export support; local runs use history.csv directly. Keep unchanged.
vocabulary.csvStarting word pairs; edit it to build your vocabulary.
6x10.bdfSupplied font; keep it beside main.py.

No test file is supplied. Create your own check.py for assertions; it imports functions from word_files.py. The game creates history.csv when it saves the first answer. Keep tests in separate files such as test_vocabulary.csv and test_history.csv.

Test one step at a time

Write your own checks as you complete each function. For example, start check.py with a small input and its predicted result for Step 1:

from word_files import load_vocabulary
with open("test_vocabulary.csv", "w", encoding="utf-8", newline="") as file:
file.write("finnish,english\nyö,night\n")
assert load_vocabulary("test_vocabulary.csv") == [
{"finnish": "yö", "english": "night"}
]
print("Checks passed.")

Save the file. In the IDE terminal, change to the exercise folder; dir or ls should show word_files.py and check.py. Run py check.py on Windows or python3 check.py on macOS/Linux, using python if that is your working command. The check above prints Checks passed. when Step 1 handles this input. Add your own cases for each step. These file-function checks need only Python, without Pyxel.

Use separate test filenames, such as test_vocabulary.csv and test_history.csv, so your tests do not overwrite learning history. Check ordinary, empty, and invalid inputs. Leave later function stubs in place while working; importing them does not call them.

Submit after each step and use its grading feedback to investigate failures. No test script is supplied, and main.py launches the game without running your checks automatically.

Rules for all steps

Use csv.DictReader(..., strict=True) and csv.DictWriter, UTF-8, and newline="" for CSV files. Field-name constants and validators are supplied in word_rules.py. Missing or extra fields are invalid. File, decoding, and CSV parsing errors propagate; application rule violations raise ValueError.

Your functions must not print, ask for input, use Pyxel, or change the caller’s records. Imports must not read or write data files. Parent folders need not be created. Protection against storage failure after writing begins is outside this exercise.

Step 1: Load vocabulary — 15 points

Implement load_vocabulary(path). Read exactly the header finnish,english, in that order. Apply checked_vocabulary_row to every row and return the trimmed pair dictionaries in file order.

Reject repeated Finnish or English words after normalized comparison with ValueError: each word has one translation in either direction. A header-only file returns []; an absent file raises FileNotFoundError.

Check ordinary pairs, Finnish letters, quoted commas, an empty table, and duplicate words. Submit when this step is ready.

Step 2: Load earlier attempts — 20 points

Implement load_history(path). Read exactly direction,source,target,response,correct. A missing file or header-only file returns []. An empty or damaged existing file is invalid; do not silently skip invalid rows.

Require exactly "0" or "1" in the correct column, convert it to an integer, then apply checked_attempt to each row. Return the checked records in file order.

The supplied validator accepts directions fi-en and en-fi, nonempty trimmed source/target text, and a response string preserved exactly. Correct must be integer 0 or 1, not a boolean, and agree with answer_matches(response, target). Matching ignores surrounding whitespace and capitalization while preserving accents. Extra fields are rejected. Old attempts remain valid when their word pair is removed from today’s vocabulary.

Check both directions, missing and header-only files, and a valid row followed by an invalid row. Run your checks before submitting.

Step 3: Append a new attempt — 20 points

Implement append_attempt(path, attempt). Check the attempt with checked_attempt and validate any existing history by calling load_history before opening for append.

Append one normalized record and return None. Create a missing file with one header; preserve every existing row. If valid existing text has no final newline, add a line separator before the new row. Invalid data must neither change an existing file nor create a new one.

Check repeated appends, the single header, a missing final newline, and rejected writes that leave the file unchanged. Run your checks before submitting.

Step 4: Summarize history and complete the game — 15 points

Implement summarize_history(history) for already-checked attempt records. Return {"attempts": ..., "correct": ..., "incorrect": ..., "by_word": [...]}. The first three values are total counts.

Each by-word record contains direction, source, target, attempts, and incorrect. Group with the supplied same_question: the direction and both normalized words must match. Preserve first-appearance group order and the first source/target spelling. Empty history gives zero counts and an empty group list. Results must be independent of the input.

Check empty history, repeated questions, opposite directions, and unchanged input records. This step’s grader also checks the supplied review mode, saving one attempt per answer, statistics in the graphical caller, failed saves, and the supplied history support.

The supplied 6x10.bdf is a Latin-1 subset of Misc Fixed 6×10, a public-domain font that displays ä, ö, Ä, and Ö. Its public-domain notice is included in the font file. Keep it beside main.py; the supplied interface loads it automatically. You do not need to edit or install the font.

After your checks pass, run main.py to play. Menu keys 1/2 choose direction, 3/4 practise mistakes, 5 shows statistics, and 6 shows the history filename. Type an answer, use Backspace to edit, and Enter to submit and advance. Escape returns to the menu.

Try a correct answer and a mistake, then start another round and compare its statistics with your prediction. Add or remove pairs in vocabulary.csv and run the game again: new questions use the current vocabulary, while old history remains. Review mode selects matching current pairs in the chosen direction, ordered by mistake count with vocabulary order breaking ties.

Run the game locally

After your function checks pass, check py --version on Windows or python3 --version on macOS/Linux. Pyxel 2.9.9 requires Python 3.11 or newer; update Python first if needed, following the setup chapter. Install Pyxel in a virtual environment in this exercise folder.

On Windows:

py -m venv .venv
.\.venv\Scripts\python.exe -m pip install pyxel==2.9.9
.\.venv\Scripts\python.exe main.py

On macOS/Linux:

python3 -m venv .venv
.venv/bin/python -m pip install pyxel==2.9.9
.venv/bin/python main.py

If the environment command reports missing venv or ensurepip support, follow its error message to install your system’s Python virtual-environment support, then retry.

Create the environment and install Pyxel once. On later runs, use only the final command. To run your assertions with the same environment, replace main.py with check.py in that command. The game does not run your assertions automatically. Close the game window when finished.

No history.csv is supplied: your reader returns an empty history when it is missing, and your writer creates it on the first saved answer. The file remains in the exercise folder between runs. Open it in your IDE after playing and compare its rows with the answers you gave. Use separate test history so checks do not overwrite your game history. Malformed saved history is not silently replaced with an empty list.

Submit your solution

Save your files. Submit using Submit Current Exercise in the IDE plugin, review the file list, and confirm Submit. Alternatively, create a ZIP from the exercise folder:

SystemCommand
Windowspy -m zipfile -c submission.zip word_files.py main.py word_rules.py browser_history.py vocabulary.csv 6x10.bdf
macOS/Linuxpython3 -m zipfile -c submission.zip word_files.py main.py word_rules.py browser_history.py vocabulary.csv 6x10.bdf

Upload submission.zip below and press Submit. The listed files must be at the archive’s top level, without an enclosing folder. Include the supplied support and sample files; leave out generated outputs, .venv, and __pycache__. Read the grading feedback after submitting. The grader also uses its own inputs; passing the sample checks alone does not establish the whole contract.

You may also include your own check.py by adding its filename to the ZIP command. Keep history.csv and your generated test data on your computer. The grader does not require a particular test function or a call from main.py.

Play and Inspect the History

Run your own checks first, then use the Pyxel setup and launch commands below to run main.py. The game does not run a test script automatically.

Choose a direction with menu key 1 or 2. Type an answer, use Backspace to correct it, and press Enter. Read the feedback, then press Enter for the next question. Escape returns to the menu. Close the local game window when finished.

Try one correct answer and one deliberate mistake. Open statistics with menu key 5 and compare the counts with your predictions. Start another round: it must add to the history rather than reset it.

The actual history.csv file remains in the game folder between runs. Menu key 6 shows its filename. Open it in your IDE after playing and connect each row with an answer you gave. Close and restart the game to check that earlier attempts still appear in the statistics.

Run Pyxel Locally

The file functions and your assertions need only standard Python. Playing locally also needs Pyxel. Check py --version on Windows or python3 --version on macOS/Linux: Pyxel 2.9.9 requires Python 3.11 or newer. Update Python first if needed, following Chapter 2. In the game folder, create a virtual environment: a separate Python environment for this project’s installed packages.

On Windows:

py -m venv .venv
.\.venv\Scripts\python.exe -m pip install pyxel==2.9.9
.\.venv\Scripts\python.exe main.py

On macOS/Linux:

python3 -m venv .venv
.venv/bin/python -m pip install pyxel==2.9.9
.venv/bin/python main.py

If creating the environment reports that venv or ensurepip is unavailable, follow the error message to install your system’s Python virtual-environment support, then retry.

Create the environment and install Pyxel once; on later runs, use only the final command. These commands use the environment’s interpreter directly; activation is not required. The pinned version matches the course’s Pyxel runtime. Keep the supplied source files, vocabulary file, and 6x10.bdf font in the game folder. If your system needs additional setup, consult Pyxel’s installation instructions.

Run main.py to play. To check the file functions separately, run .\.venv\Scripts\python.exe check.py on Windows or .venv/bin/python check.py on macOS/Linux. Keep personal history, generated test files, and .venv on your computer; use the handout’s file list when submitting through the IDE plugin or as a ZIP.

Build Your Vocabulary and Practise Mistakes

Add a new pair to vocabulary.csv, keeping the header, then run the game again. Use correctly quoted CSV for a field containing a comma. Remove another pair and check that it disappears from new questions while its earlier history remains.

Menu keys 3 and 4 use the supplied review mode. It selects current word pairs with recorded mistakes in that direction, ordered by mistake count. Vocabulary order breaks ties. It does not recommend deleted pairs or silently attach an old attempt to a changed translation.

A count is a simple starting rule. Five mistakes in fifty attempts differ from five mistakes in five attempts. As an optional extension, compare mistake counts with error rates and include opportunities to practise words with no history. Another extension is an in-game vocabulary editor using the same file contract.

Check Your Understanding

  1. Why are vocabulary and history separate files?
  2. What would happen if every new answer opened history in "w" mode?
  3. Why does an old attempt keep its expected translation?
  4. Which functions can be tested without starting Pyxel?
  5. Why do opposite translation directions need separate statistics?