diff --git a/STAGING-NOTES.md b/STAGING-NOTES.md new file mode 100644 index 0000000..a4c1b21 --- /dev/null +++ b/STAGING-NOTES.md @@ -0,0 +1,46 @@ +# Staging notes (work branch) — Memnar #1118 / #1119 + +Treat **https://tanco.tangentcode.com/** as staging while on branch `work`. +Nobody else is using it for this verification window. + +## What was broken (2026-09-18) + +1. **tanco.service crash-loop (203/EXEC)** + Venv at `/home/michal/ver/tanco/.venv` pointed at `/bin/python3.11`, which had been removed from the host (deadsnakes packages purged). Process had kept running on a deleted inode until restart, then failed permanently. Site returned 502; `POST /auth/pre` had previously returned 500 under the dying process. + +2. **Empty challenge DB** + `~/.tanco.sdb` had 0 challenges / 0 tests, so `/c.json` was `[]` and `tanco init` could not select `learntris-classic`. + +3. **Import path not a drop-in restore** + `tanco migrate` + `tanco import` on `/home/michal/ver/learntris/testplan.org` failed (incomplete migrate; invalid test name `nudge.<`). Used known-good backup DB instead. + +## What we changed on staging (ops, not live "production" intent) + +- Created git branch `work` from `main` (`a447580`). +- Rebuilt `.venv` with **Python 3.12.3** and `pip install -e .`. +- Old broken venv renamed to `.venv-py311-broken-YYYYMMDD`. +- Restored DB: copied `~/.tanco-as-server.sdb` → `~/.tanco.sdb` (45 tests, `learntris-classic`). Empty DB backed up as `~/.tanco.sdb.empty-backup-*`. +- `sudo systemctl restart tanco.service` — active, `/` 200, `POST /auth/pre` 200, `/c.json` lists learntris-classic. + +## Suggested durable follow-ups (code / ops) + +- Pin or document runtime Python (3.12+) in README / unit file; avoid hard dependency on a removed 3.11 binary. +- Consider committing a bootstrap/seed path for `learntris-classic` (or fix migrate/import for `nudge.<` and full v0.1→v0.2). +- Client `tanco login` opens browser silently and blocks on `auth/jwt` with no printed URL — setup page should note the URL pattern `/auth/login?pre=...` for headless environments. +- Setup page command list is stale vs tanco 0.4.0 (missing: bind, migrate, run, send, share, spawn, status, version; wording diffs on check/next/show/test). + +## Not changed + +- No nginx / cert changes. +- No Firebase / Google OAuth client changes. +- Did not push `work` to origin unless explicitly requested after review. + +## Memnar #1128 — learntris-classic v0.1 to v0.2 (2026-09-18) + +- Suite source: /home/michal/ver/learntris/testplan.org (was v0.1: #+name: plus = / : lines inside src). +- Upgrade path: tanco migrate in this repo (tanco/driver.py). +- Migrate fixes on work: look ahead past prose for #+name:; preserve pre-src commentary; rename illegal nudge.< / nudge.> to nudge.left / nudge.right; count TEST headlines at any star level. +- Seed copy: suites/learntris-classic.org (45 tests, #+tanco-format: 0.2). +- Staging DB: deleted old challenge plus attempts/progress; tanco import suites/learntris-classic.org yields 45 tests. +- Verified without Google OAuth: /c.json lists course; JWT attempt plus /next plus /check/io.q pass/fail plus next to io.p; /c/learntris-classic 200. +- No merge to main. Restart of tanco.service not required (sqlite file reload). diff --git a/suites/learntris-classic.org b/suites/learntris-classic.org new file mode 100644 index 0000000..722ff96 --- /dev/null +++ b/suites/learntris-classic.org @@ -0,0 +1,1472 @@ +#+title: learntris test suite +#+tanco-format: 0.2 +#+server: https://tanco.tangentcode.com/ +#+name: learntris-classic + +# This file is created with org-mode, an awesome outliner and +# personal information manager for emacs. In org-mode, this file +# will appear as a foldable outline, but if you don't want to use +# emacs, it's fine to just edit it by hand. + +#+todo: TODO | DONE TEST + +* DONE [4/4] basic i/o +** TEST io.q : quit + +#+begin_src +> q +#+end_src + +The 'q' command instructs learntris to quit. + +Learntris should not produce any output unless +explicitly instructed to do so. + +** TEST io.p : print + +Matrix is the official term for the 10 \times 22 grid of cells. + +The playing area is only 10 \times 20 but there are 2 extra lines +above where new tetriminos form. + +#+begin_src +> p +# 1 2 3 4 5 6 7 8 9 ( first column, hidden by the #, is 0 +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +> q +#+end_src + +The 'p' command instructs learntris to print the state +of the matrix, the rectangular array of cells in which +blocks can appear. + +The matrix is 10 cells wide and 22 cells deep, although +the top two rows are used only for spawning new Tetraminos. + +At the start of the game, the matrix should be empty. +The 'p' command should indicate empty cells with the +'.' character. + +Cells should be separated by spaces. + +Lines should be separated by the standard end of line +sequence on your operating system (python's "\n"). + +** TEST io.g : given + +#+begin_src +> g +> . . . . . . . . . . # 0 +> . . . . . . . . . . # 1 +> . . . . . . . . . . # 2 +> . . . . . . . . . . # 3 +> m m m m m m m m m m # 4 +> b b b b b b b b b b # 5 +> c c c c c c c c c c # 6 +> g g g g g g g g g g # 7 +> y y y y y y y y y y # 8 +> o o o o o o o o o o # 9 +> r r r r r r r r r r # 10 +> . . . . . . . . . . # 11 +> . . . . . . . . . . # 12 +> . . . . . . . . . . # 13 +> . . . . . . . . . . # 14 +> c . . . . . . . . . # 15 +> c . . . . . . . . . # 16 +> c . . . . g . . . . # 17 +> c . . o . g g . . . # 18 +> . . . o . b g . . . # 19 +> . m r r o o b y y . # 20 +> m m m r r b b y y . # 21 +> p +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +m m m m m m m m m m # 4 +b b b b b b b b b b # 5 +c c c c c c c c c c # 6 +g g g g g g g g g g # 7 +y y y y y y y y y y # 8 +o o o o o o o o o o # 9 +r r r r r r r r r r # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +c . . . . . . . . . # 15 +c . . . . . . . . . # 16 +c . . . . g . . . . # 17 +c . . o . g g . . . # 18 +. . . o . b g . . . # 19 +. m r r o o b y y . # 20 +m m m r r b b y y . # 21 +> q +#+end_src + +The 'g' command instructs learntris to read 22 lines +of text from the standard input stream, and use the +characters on these lines to populate some internal +representation of the matrix. + +The letter 'g' is a mnemonic for the word 'given', as +in: "given the following matrix...." + +The input format should be identical to the output +produced by the 'p' command. + +The letters used in the representation correspond to +the set of colors used for blocks in the game: + +. = empty (black) b = blue c = cyan +g = green m = magenta o = orange +r = red y = yellow + +** TEST io.c : clear + +#+begin_src +> g +> . . . . . . . . . . # 0 +> . . . . . . . . . . # 1 +> . . . . . . . . . . # 2 +> . . . . . . . . . . # 3 +> m m m m m m m m m m # 4 +> b b b b b b b b b b # 5 +> c c c c c c c c c c # 6 +> g g g g g g g g g g # 7 +> y y y y y y y y y y # 8 +> o o o o o o o o o o # 9 +> r r r r r r r r r r # 10 +> . . . . . . . . . . # 11 +> . . . . . . . . . . # 12 +> . . . . . . . . . . # 13 +> . . . . . . . . . . # 14 +> c . . . . . . . . . # 15 +> c . . . . . . . . . # 16 +> c . . . . g . . . . # 17 +> c . . o . g g . . . # 18 +> . . . o . b g . . . # 19 +> . m r r o o b y y . # 20 +> m m m r r b b y y . # 21 +> c +> p +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +> q +#+end_src + +The 'c' command instructs learntris to clear the matrix. + +* DONE [5/5] basic game ideas +** DONE [2/2] establish protocol to set and query the score +*** TEST io.score.s : query 's' register (score) + +#+begin_src +> ?s +0 +>q +#+end_src + +The '?s' command instructs learntris to display the +current score as a decimal number. + +Initially, the score should be zero. + +*** TEST io.score.n : query 'n' register (number of cleared lines) + +#+begin_src +> ?n +0 +>q +#+end_src + +The '?n' command instructs learntris to display the +number of lines that have been cleared. + +Initially, this number should be zero. + +** TEST rule.clear : step +#+begin_src +> ?s +0 +> g +> . . . . . . . . . . # 0 +> . . . . . . . . . . # 1 +> . . . . . . . . . . # 2 +> . . . . . . . . . . # 3 +> . . . . . . . . . . # 4 +> . . . . . . . . . . # 5 +> . . . . . . . . . . # 6 +> . . . . . . . . . . # 7 +> . . . . . . . . . . # 8 +> . . . . . . . . . . # 9 +> m c r g b y m c o b # 10 +> . . . . . . . . . . # 11 +> . . . . . . . . . . # 12 +> m y o . c r g c m y # 13 +> . . . . . . . . . . # 14 +> . . . . . . . . . . # 15 +> . . . . . . . . . . # 16 +> . . . . . . . . . . # 17 +> . . . . . . . . . . # 18 +> . . . . . . . . . . # 19 +> . . . . . . . . . . # 20 +> . . . . . . . . . . # 21 +> s +> p +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +m y o . c r g c m y # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +> ?n +1 +> ?s +100 +> q +#+end_src + +The 's' command instructs learntris to execute one step +of the simulation. + +If the matrix contains an unbroken row of blocks, (or +in other words, a row that contains no empty cells), then +the 's' command should: + +1. Clear the row (replacing each block with an empty cell) +2. Increment the 'n' register by 1. +3. Increment the 's' register by 100. + +This is only an initial approximation of the scoring +system, but it will do for now. + +** DONE [7/7] introduce the tetraminos +*** TEST shape.I : drawing and selecting the active tetramino + +#+begin_src +> I +> t +. . . . +c c c c +. . . . +. . . . +> q +#+end_src + +At any point in time while the game is running, there +should be a single 'active' tetramino that the player +is able to control. Normally these are chosen somewhat +randomly at runtime, but for automated testing, there +needs to be some way to set the active tetramino. + +There are exactly seven tetraminos and they each have +specific names used by mathematicians and players of +video games. This one is called the I tetramino, and +in the game, it is generally colored cyan. + +After issuing the 'I' command, the active tetramino +should be the 'I' tetramino, and it should be oriented +horizontally. + +In order to verify that the active tetramino is correct, +the tests need some way to ask what it looks like. This +is the role of the 't' command: it displays the active +tetramino. + +*** TEST shape.O : The O Tetramino + +#+begin_src +> O +> t +y y +y y +> q +#+end_src + +The 2x2 square is called the O tetramino, and in the game +it should be colored yellow. + +Note that while the I tetramino appeared inside a 4x4 +matrix, the O tetramino's matrix is 2x2. This has to do +with rotation, which we will deal with later. + +*** TEST shape.Z : The Z Tetramino + +The Z and others fit in 3x3 matrices. + +#+begin_src +> Z t q +r r . +. r r +. . . +#+end_src + +The Z Tetramino is colored red and inhabits a 3x3 matrix. + +Notice that in this test, the commands are all issued on +a single line. This helps keep the test descriptions +readable, and also makes it more convienient to type the +commands interactively by hand. + +You do realize you can do that, right? :) + +Anyway, from now on, commands may be separated by either +newlines or spaces. You may need to adjust your +implementation accordingly. + +*** TEST shape.S : The S Tetramino. + +#+begin_src +> S t q +. g g +g g . +. . . +#+end_src + +The green S tetramino inhabits a 3x3 matrix. It is a mirror image of the Z. + +*** TEST shape.J : The J Tetramino + +#+begin_src +> J t q +b . . +b b b +. . . +#+end_src + +The J tetramino is blue and inhabits a 3x3 matrix. Its default +orientation is rotated 90 degrees counter clockwise. + +*** TEST shape.L : The L Tetramino + +#+begin_src +> L t q +. . o +o o o +. . . +#+end_src + +The L tetramino is a mirror image of the J. It is colored orange. + +*** TEST shape.T : The T Tetramino + +#+begin_src +> T t q +. m . +m m m +. . . +#+end_src + +The T is is colored magenta and spawns upside down in a 3x3 matrix. + +** DONE [7/7] introduce (simplified) super rotation system + +Clockwise only. No wall-kicks. For each test, four rotations should bring it back to the staring position. + +*** TEST clockwise.I : clockwise rotation + +#+begin_src +> I ) t +. . c . +. . c . +. . c . +. . c . +> ) t +. . . . +. . . . +c c c c +. . . . +> ) t +. c . . +. c . . +. c . . +. c . . +> ) t q +. . . . +c c c c +. . . . +. . . . +#+end_src + +The ')' command rotates the active tetramino 90 degrees clockwise. +Here we see the I tetramino rotating clockwise within its bounding box. + +*** TEST clockwise.O : O rotation + +#+begin_src +> O ) t +y y +y y +> ) t +y y +y y +> ) t +y y +y y +> ) t q +y y +y y +#+end_src + +The O looks the same in all orientations. + +*** TEST clockwise.Z : Z Rotation + +#+begin_src +> Z t +r r . +. r r +. . . +> ) t +. . r +. r r +. r . +> ) t +. . . +r r . +. r r +> ) t q +. r . +r r . +r . . +#+end_src + +The Z rotates within its 3x3 matrix. + +*** TEST clockwise.S : newlines and S rotation + +#+begin_src +> S t ; ) t ; ) t ; ) t q +. g g +g g . +. . . + +. g . +. g g +. . g + +. . . +. g g +g g . + +g . . +g g . +. g . +#+end_src + +You should be getting the hang of rotation now. +Here we see the new ';' command, which emits a newline +simply to make the output a little more readable. + +*** TEST clockwise.J : J Rotation + +#+begin_src +> J t ; ) t ; ) t ; ) t q +b . . +b b b +. . . + +. b b +. b . +. b . + +. . . +b b b +. . b + +. b . +. b . +b b . +#+end_src + +No surprises here. +Maybe this is a good time to look over your code +and see if there's something you ought to clean up? + +*** TEST clockwise.L : L Rotation + +#+begin_src +> L t ; ) t ; ) t ; ) t q +. . o +o o o +. . . + +. o . +. o . +. o o + +. . . +o o o +o . . + +o o . +. o . +. o . +#+end_src + +Yep. Another rotating tetramino. + +*** TEST clockwise.T : T Rotation + +#+begin_src +> T t ; ) t ; ) t ; ) t ; q +. m . +m m m +. . . + +. m . +. m m +. m . + +. . . +m m m +. m . + +. m . +m m . +. m . +#+end_src + +Last one! + +** DONE [4/4] implement movement +*** DONE [7/7] draw a shape in the matrix at the spawn point. +**** TEST spawn.O : Print Matrix with active Tetramino +#+begin_src +> c O P q +. . . . Y Y . . . . # 0 +. . . . Y Y . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +Now it's time to place the active tetramino in the matrix. +The 'P' command should print the matrix just like 'p' does, +but in addition, it should show the active tetramino. + +Note that the active tetramino is drawn with upper case +letters, so we can distinguish it from the settled blocks. + +Here we see the O tetramino in its initial spawn location. + +**** TEST spawn.L : Spawn location for L +#+begin_src +> c L P q +. . . . . O . . . . # 0 +. . . O O O . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +This is how the L Tetramino spawns. + +**** TEST spawn.J : Spawn location for J +#+begin_src +> cJPq +. . . B . . . . . . # 0 +. . . B B B . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +Here's the spawn location for J. + +Notice that there are no spaces between the commands +this time. We said earlier that commands may be separated +by spaces or newlines, but we never said they *had* to be. + +**** TEST spawn.Z : Spawn Location for Z +#+begin_src +> cZPq +. . . R R . . . . . # 0 +. . . . R R . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +This is where the Z spawns. + +**** TEST spawn.S : Spawn Location for S +#+begin_src +> cSPq +. . . . G G . . . . # 0 +. . . G G . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +This is where the S spawns. + +**** TEST spawn.I : Spawn Location for I +#+begin_src +> cIPq +. . . . . . . . . . # 0 +. . . C C C C . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +This is where the I spawns. + +**** TEST spawn.T : Spawn Location for T +#+begin_src +> cTPq +. . . . M . . . . . # 0 +. . . M M M . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +And finally, this is where the T spawns. + +*** DONE [3/3] basic left/right/down movement ( < v > ) +**** TEST nudge.left : It's just a step to the left... +# migrated from #+name: nudge.< + +#+begin_src +> cT + +#+begin_src +> cT>Pq +. . . . . M . . . . # 0 +. . . . M M M . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +The '>' command nudges the tetramino one cell to the right. + +**** TEST nudge.v : Onward, young Tetramino + +#+begin_src +> cTvPq +. . . . . . . . . . # 0 +. . . . M . . . . . # 1 +. . . M M M . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +The 'v' command nudges the active tetramino downward by one cell. + +*** DONE [6/6] collision detection for the floor and walls +**** TEST collide.left-wall : Hitting The Wall + +#+begin_src +# nudging <<< should put T on the left wall +> cT<<

>>> should put T on the right wall +> cT>>>>P +. . . . . . . . M . # 0 +. . . . . . . M M M # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +# nudging again should do nothing: +> >Pq +. . . . . . . . M . # 0 +. . . . . . . M M M # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +Look out! It seems that crafty T tetramino is trying to escape +on the right side of the matrix! Let's teach it a lesson, shall we? + +**** TEST collide.right-wall-2 : Smashing its Head Against the Wall + +#+begin_src +> T ( >>>> > Pq +. . . . . . . . . M # 0 +. . . . . . . . M M # 1 +. . . . . . . . . M # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +This T appears to have rotated counter-clockwise +with the '(' command, and then attempted once again +to plow its way through the right side of the matrix. + +It gets an extra '>' in with this strategy, but +there can be no compromise. + +**** TEST collide.floor : Oh, how the Mighty have Fallen... + +#+begin_src +> cT vvvv vvvv vvvv vvvv vvvv vvvv Pq +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . M . . . . . # 20 +. . . M M M . . . . # 21 +#+end_src + +Once upon a time, a T tetramino spawned at the +top of the well, and used the 'v' command to +move down by one cell 24 times. Sadly, it was +unable to burrow into the earth and spent the +remainder of its life standing on its head at +the bottom of a well. + +**** TEST drop.floor : Taking the plunge. +#+begin_src +> TVpq +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . m . . . . . # 20 +. . . m m m . . . . # 21 +#+end_src + +This one dove head first into the floor, +using the 'V' command to trigger a hard drop. + +Note that we're printing with 'p' this time. +There's no walking away from a hard drop. + +**** TEST drop.floor.rotated : Rotate and Hard drop. +#+begin_src +> T)V pq +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . m . . . . . # 19 +. . . . m m . . . . # 20 +. . . . m . . . . . # 21 +#+end_src + +Aside from demonstrating the astonishing grace +and agility of your average tetramino as it plunges +sideways to its doom, this test double checks that +your collision detection holds up under rotation. + +*** DONE [4/4] collision detection for other blocks +**** TEST drop.collide : Block Collisions +#+begin_src +> TV ZV pq +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . r r . . . . . # 18 +. . . . r r . . . . # 19 +. . . . m . . . . . # 20 +. . . m m m . . . . # 21 +#+end_src + +Here we have our first test of collision detection +between a falling piece and a block already in the matrix. + +When the second hard drop is issued, the Z tetramino +should collide with the remains of the T and come to +a complete halt. + +**** TEST drop.collide.rotated : Rotated Collision +#+begin_src +> TV Z) J ) << V Z ) vvvvvvvvvvvvvvvvv < Pq +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . R . . . . # 17 +. . . . R R . . . . # 18 +. . b b R . . . . . # 19 +. . b . . . . . . . # 20 +. . b . . . . . . . # 21 +#+end_src + +Here we've hard-dropped a J off to the left side a bit, then +spawned a Z and manually moved it down next to the fallen blocks. +The '<' commands should have no effect. + +**** TEST collide.right-block : Sideways Block Collisions 2 +#+begin_src +> I)>V I)>V I)>V I)>V T)>vvvv> Pq +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . M . . . . # 4 +. . . . . M M . . . # 5 +. . . . . M c . . . # 6 +. . . . . . c . . . # 7 +. . . . . . c . . . # 8 +. . . . . . c . . . # 9 +. . . . . . c . . . # 10 +. . . . . . c . . . # 11 +. . . . . . c . . . # 12 +. . . . . . c . . . # 13 +. . . . . . c . . . # 14 +. . . . . . c . . . # 15 +. . . . . . c . . . # 16 +. . . . . . c . . . # 17 +. . . . . . c . . . # 18 +. . . . . . c . . . # 19 +. . . . . . c . . . # 20 +. . . . . . c . . . # 21 +#+end_src + +Here's another scenario. That rascally T tetramino +is attempting to pass through a tower of cyan blocks. +Alas, his big head gets in the way... Or at least it +would, if this test were passing... + +* DONE [2/2] screenflow +** TEST screenflow.title : The Title Screen +#+begin_src +> @ p +Learntris (c) 1992 Tetraminex, Inc. +Press start button to begin. +> ! p q +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +Legal says we need a title screen with a copyright notice. +It should show up when you first turn on the game, but for +the tests, they're fine as long as there's some way to get +to it. + +Here's what we came up with: the '@' command takes you to +the title screen, where the 'p' command prints out the main +menu. Then '!' to simulate the start button should take you +to the game. + + + + +** TEST screenflow.pause : The Pause Menu +#+begin_src +> p +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +> ! p +Paused +Press start button to continue. +> ! p q +. . . . . . . . . . # 0 +. . . . . . . . . . # 1 +. . . . . . . . . . # 2 +. . . . . . . . . . # 3 +. . . . . . . . . . # 4 +. . . . . . . . . . # 5 +. . . . . . . . . . # 6 +. . . . . . . . . . # 7 +. . . . . . . . . . # 8 +. . . . . . . . . . # 9 +. . . . . . . . . . # 10 +. . . . . . . . . . # 11 +. . . . . . . . . . # 12 +. . . . . . . . . . # 13 +. . . . . . . . . . # 14 +. . . . . . . . . . # 15 +. . . . . . . . . . # 16 +. . . . . . . . . . # 17 +. . . . . . . . . . # 18 +. . . . . . . . . . # 19 +. . . . . . . . . . # 20 +. . . . . . . . . . # 21 +#+end_src + +Wow, that title screen you whipped up was a huge hit upstairs. +They loved pressing that start button, and they want to show +it off in the brochures. So now they're asking for a pause +screen that pops up when the '!' command gets issued in-game. +Sending '!' again should unpause. + +**** TEST gameover : The Game Over Screen +#+begin_src +> I)>V I)>V I)>V I)>V I)>V T)>>V Pq +. . . . . m . . . . # 0 +. . . . . m m . . . # 1 +. . . . . m c . . . # 2 +. . . . . . c . . . # 3 +. . . . . . c . . . # 4 +. . . . . . c . . . # 5 +. . . . . . c . . . # 6 +. . . . . . c . . . # 7 +. . . . . . c . . . # 8 +. . . . . . c . . . # 9 +. . . . . . c . . . # 10 +. . . . . . c . . . # 11 +. . . . . . c . . . # 12 +. . . . . . c . . . # 13 +. . . . . . c . . . # 14 +. . . . . . c . . . # 15 +. . . . . . c . . . # 16 +. . . . . . c . . . # 17 +. . . . . . c . . . # 18 +. . . . . . c . . . # 19 +. . . . . . c . . . # 20 +. . . . . . c . . . # 21 +Game Over +#+end_src + +The T tetramino couldn't escape the top 2 tetramino +spawning rows. Your game is over! + +* TEST learntris.end : The Next Test +#+begin_src +> q +(Something Awesome) +#+end_src + +Congratualations! Your learntris implementation passes all +existing tests, and you've even created and passed a few of +your own. + +Take a moment to pat yourself on the back for making it this far. :) + +Now it's time for the real test... + +Take a look at what you've built so far. Is this everything +you'd want to see in a video game? Or is it missing a thing +or two? + +This test case contradicts the very first one, so the only way +to make it pass is to delete it, or replace it with something +better. The choice is yours. + +Either way, you will need to open up testplan.org and search +for the test named 'learntris.end'. + + +* NOTE How to add a new test. + +- Think of a situation that needs to be tested. +- Think of the commands you would need to issue to make that situation happen. +- Write a new test that shows the expected behavior. + +Here is the test format (remove the leading ': ' from each line, and also the comments on the right). + +: #+name: test.name <- must be unique! +: #+begin_src <- marks start of test +: # other lines with # are comments <- comment +: > o <- '>' indicates an input line +: output <- anything besides (#, >, :, or =) +: more output | is expected output. +: > q <- always send the quit command! +: = my very own test <- this is the title for the test +: : This is where I describe the test. <- ':' indicates a line of description +: : Here's another descriptive line. | these lines show up when a test fails. +: #+end_src <- marks end of test + +* TODO [0/1] Ideas for more tests + +- [x] Game over conditions. +- [ ] Better scoring. +- [ ] Some or all of the [[http://tetrisconcept.net/wiki/SRS][Super Rotation System]] + - [ ] Wall Kicks + - [ ] Floor Kicks + - [ ] T-Spins +- [ ] The [[http://tetris.wikia.com/wiki/Ghost_piece][Ghost Piece]] (preview of where the tetramino will land) +- [ ] The [[http://tetris.wikia.com/wiki/Hold_piece][Hold Piece]] (swap out the active tetramino) +- [ ] Test how and when the game speeds up as you progress. +- [ ] A high score system. +- [ ] Rules for when a tetramino becomes part of the matrix. + +... Or come up with your own rules and make the game your own. :) + diff --git a/tanco/driver.py b/tanco/driver.py index 86f9ac7..781cbe5 100644 --- a/tanco/driver.py +++ b/tanco/driver.py @@ -111,6 +111,7 @@ def do_import(arg): @staticmethod def do_migrate(arg): """Migrate an org file from v0.1 to v0.2 format""" + import re parser = argparse.ArgumentParser(prog='migrate', description='Migrate org file to v0.2 format') parser.add_argument('--check', action='store_true', help='Preview changes without modifying file') parser.add_argument('file', nargs='?', help='Org file to migrate') @@ -145,7 +146,7 @@ def do_migrate(arg): migrated_lines = _migrate_org_file(lines) # Count changes - num_tests = sum(1 for line in migrated_lines if line.strip().startswith('** TEST ')) + num_tests = sum(1 for line in migrated_lines if re.match(r'\*+\s+TEST\s+', line.strip())) num_name_removed = sum(1 for i, line in enumerate(lines) if line.strip().startswith('#+name:') and any(i+j < len(lines) and lines[i+j].strip().startswith('#+begin_src') @@ -548,6 +549,24 @@ def _run_tests_with_error_handling(cfg, names=None): runner.handle_unexpected_error(cfg) +def _sanitize_test_name(name): + """Rewrite v0.1 names that are illegal in v0.2 (only [a-zA-Z0-9._-] allowed).""" + import re + # learntris-classic used nudge.< / nudge.> for left/right key tests + replacements = { + 'nudge.<': 'nudge.left', + 'nudge.>': 'nudge.right', + } + if name in replacements: + return replacements[name] + if re.match(r'^[a-zA-Z0-9._\-]+$', name): + return name + table = str.maketrans({'<': 'lt', '>': 'gt', '/': '-', ' ': '-'}) + cleaned = name.translate(table) + cleaned = re.sub(r'[^a-zA-Z0-9._\-]', '', cleaned) + return cleaned or 'unnamed' + + def _migrate_org_file(lines): """Migrate org file lines from v0.1 to v0.2 format.""" import re @@ -590,44 +609,41 @@ def _migrate_org_file(lines): migrated.append('#+tanco-format: 0.2\n') format_added = True - # Add #+todo: line if we've added format and haven't added todo yet - # Add it when we hit the first headline (starts with *) + # Add #+todo: when we hit the first headline if format_added and not todo_added and line.startswith('*'): - # Check if #+todo: already exists in original file has_todo = any(l.strip().lower().startswith('#+todo:') for l in lines) if not has_todo: migrated.append('#+todo: TODO | DONE TEST\n') migrated.append('\n') todo_added = True - # Check if this is a test headline by looking for #+name: nearby + # Test headline: #+name: anywhere before the next headline if line.startswith('*'): - # Look ahead for #+name: within next 5 lines test_name = None - for j in range(i+1, min(i+6, len(lines))): + name_idx = None + for j in range(i+1, len(lines)): + if lines[j].startswith('*'): + break if lines[j].strip().startswith('#+name:'): test_name = lines[j].strip().split(':', 1)[1].strip() + name_idx = j break - elif lines[j].startswith('*'): - break # Hit next headline if test_name: - # This is a test headline - migrate it - # Extract headline info match = re.match(r'^(\*+)\s*(?:TODO|DONE)?\s*(.*)$', line) stars = match.group(1) if match else '**' headline_text = match.group(2).strip() if match and match.group(2) else '' - # Find #+begin_src and collect content begin_idx = None - for j in range(i+1, min(i+15, len(lines))): + for j in range(i+1, len(lines)): + if lines[j].startswith('*'): + break if lines[j].startswith('#+begin_src'): begin_idx = j break - if begin_idx: - # Collect test content - title = None # Title from = line + if begin_idx is not None: + title = None desc_lines = [] test_content = [] @@ -642,46 +658,47 @@ def _migrate_org_file(lines): test_content.append(lines[end_idx]) end_idx += 1 - # Determine final title - # The v0.1 = line format is often "= label : description" - # where label is redundant with the test name, so strip it + # Preserve prose between headline and #+begin_src (except #+name:) + pre_src = [] + for j in range(i+1, begin_idx): + if j == name_idx: + continue + pre_src.append(lines[j]) + if title and ':' in title: final_title = title.split(':', 1)[1].strip() else: final_title = title if title else headline_text - # Build migrated test + safe_name = _sanitize_test_name(test_name) + if final_title: - migrated.append(f'{stars} TEST {test_name} : {final_title}\n') + migrated.append(f'{stars} TEST {safe_name} : {final_title}\n') else: - migrated.append(f'{stars} TEST {test_name}\n') + migrated.append(f'{stars} TEST {safe_name}\n') - # Add blank line if there was one - if i+1 < len(lines) and lines[i+1].strip() == '': - migrated.append('\n') + if safe_name != test_name: + migrated.append(f'# migrated from #+name: {test_name}\n') + + for pre in pre_src: + migrated.append(pre) - # Add #+begin_src migrated.append(lines[begin_idx]) - # Add test content (without = and : lines) for content_line in test_content: migrated.append(content_line) - # Add #+end_src if end_idx < len(lines): migrated.append(lines[end_idx]) - # Add description as plain text if desc_lines: migrated.append('\n') for desc in desc_lines: migrated.append(desc + '\n') - # Skip past this test i = end_idx + 1 continue - # Not a test, keep line as-is migrated.append(line) i += 1