From 1e514f679b454331cf34c0f4503a420e112a36ac Mon Sep 17 00:00:00 2001 From: Tachikoma Date: Mon, 3 Aug 2026 22:57:57 -0400 Subject: [PATCH] Rework the megademo to fit the 80-column source line limit AKBASIC_MAX_LINE_LENGTH's cut from 256 to 80 left seventeen lines of examples/megademo unloadable: the sixteen IM$() picture strings (up to 252 characters) and TUNEA/TUNEB's four-bar PLAY strings (174 and 175). The real ceiling is 78 characters, not 80 -- stdio_readline() refuses a read that fills the 80-byte buffer without a newline, so content plus its terminator must fit in 79. The picture: vaporwave.py's PAYLOAD drops from 240 to 64, so every emitted IM$(NN) = "..." line fits under the ceiling. chop() no longer slices blind; it walks the stream a record at a time -- two characters for a run, three for an R row record -- and never cuts inside one, because the decoder reads a record's tail with MID on the string it is walking and a record straddling two IM$ entries decodes as garbage. The old blind slice at 240 only happened to be safe. verify() now simulates the CHOPPED strings with the cursor threaded across the boundaries exactly the way DRAWSTREAM executes them, so a bad cut is an assertion failure instead of a corrupted screen, and emit_block() asserts every emitted line fits. The picture is 56 strings where it was 16; the decoder needed no changes at all, since it already carries X#/Y# from one IM$ entry to the next. The music: TUNEA and TUNEB each become four PLAY statements, one bar apiece. play.c keeps voice, envelope, level and duration state on the runtime across statements and every PLAY appends to the same queue, so four bars queue exactly as one long string did. Each bar restates the V1T3U9S prefix so a bar dropped by QFULL cannot leave the next batch playing on the drum kit's envelope. Everything still clears the shrunken pools with room to spare: 1625 source lines of 2048, ~704 array slots of 2048, identifiers within the 24-character symtab key. Verified end to end against this branch's build: the demo loads, the offscreen host renders every scene, and the scene-5 still is pixel-identical to vaporwave.py's own preview. The test suite fails the same seventeen cases with and without this commit. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01ACffnV6F7sxQuG3Y8a1L3s --- examples/megademo/README.md | 9 ++- examples/megademo/megademo.bas | 116 ++++++++++++++++++++++++--------- examples/megademo/vaporwave.py | 68 +++++++++++++++---- 3 files changed, 146 insertions(+), 47 deletions(-) diff --git a/examples/megademo/README.md b/examples/megademo/README.md index e5c9a27..cf1d948 100644 --- a/examples/megademo/README.md +++ b/examples/megademo/README.md @@ -56,7 +56,10 @@ This is a tour of the interpreter's edges, on purpose: counterpoint: a genuine 24x21 `SPRSAV` type-in sprite, 63 bytes, which is everything `DATA` has room to say. The paint is the reveal, and the exit is a stride-63 column dissolve — 63 is coprime to 160, so the walk hits every - column once and looks random while carrying no state. + column once and looks random while carrying no state. The strings come in + 64-character chunks because a source line is 80 columns, like the machines + this pretends to be — the decoder carries its cursor from one `IM$` entry to + the next, and the generator refuses to cut inside a run. - **And then the picture is video.** Six delta frames loop the floor grid toward you and crawl the sun's slice pattern — full motion video, 1.3 KB total, about 220 bytes a frame. A delta re-encodes only the rows that changed: an `R` @@ -82,6 +85,10 @@ This is a tour of the interpreter's edges, on purpose: batch a bar of noise drums. Noise is real: `ENVELOPE`'s sixth argument is a waveform, 0 to 3, which Chapter 7 forgot to mention. Voice 1 carries the tune so the collision blips (voice 2) and scene sweeps (voice 3) never steal its channel. + A batch is four `PLAY` statements, one bar each — the 80-column line limit will + not hold four bars in one string, and it does not need to: the parser's voice, + envelope and duration state persists across statements and every `PLAY` appends + to the same queue, so four bars queue exactly as one line would. It also probes for its hardware like a proper boot loader, with one `TRAP` per device: no graphics refuses by name and exits, no audio mutes the soundtrack, no diff --git a/examples/megademo/megademo.bas b/examples/megademo/megademo.bas index 2e49179..1b58fb4 100644 --- a/examples/megademo/megademo.bas +++ b/examples/megademo/megademo.bas @@ -461,38 +461,78 @@ BS% = W# / 160 IF BS% < 1 THEN BS% = 1 REM ---- PICTURE-BEGIN (generated by vaporwave.py; do not REM ---- hand-edit -- rerun the script to change the picture) -DIM IM$(16) +DIM IM$(56) DIM VA#(6) DIM VB#(6) -NS# = 9 -VA#(0) = 9 -VB#(0) = 9 -VA#(1) = 10 -VB#(1) = 10 -VA#(2) = 11 -VB#(2) = 12 -VA#(3) = 13 -VB#(3) = 13 -VA#(4) = 14 -VB#(4) = 14 -VA#(5) = 15 -VB#(5) = 15 -IM$(0) = "G9G9G9G9GPGHEHG9GTEHG9GTEHGPGPEHG9GTEHG9GTEHGHGXEHGTBAG8EHG9GTEHG5EHGPEHG5EHGPEHG5EHGAPAG3EHGPEHG5EHGPEHGXGHEHG5EHGPEHG5EHGFBAGIEHGPGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGBPAGMEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEPGHEHGPEH" -IM$(1) = "GHEPGHEHGPEHGHEPGHEHGHEGBAEHGHEHGPEHGHEPGHEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPECPAEDGHEPGHEHGHEPGHEPGHEHGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHECBAEDGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHEHGHEPGHEPEPGHEPGHE5GHEPGHE5GHEHEXGHEPGHEEPAEZGHEPGHE5GHE5GHEPGHE5GHEP" -IM$(2) = "GHE5GHE9ETGHE9ETGHEXEHGHE9ETGHHAE9ESGHEPEPGBBAGEE9EMHOE9ETGHEHE9E7HUE9E6E9E4H0E9E3E9E3H2E9EHBAETEHKHE9ELH6E9ECKHEPEPKHE9EBH9HAE9EIKHEHEXKHE2H9HCE9EPKHE5KHEPKDH9HEKCEPKHE5KHE5KHEKH9HGEBKHEPKHEXEHKHE5KHEBH9HIEIKHEPKHEPEPKHEPKHEHKAH9HKKHEHKHEP" -IM$(3) = "KHEHKHEPKHEPKHH9HMEGKHEHKHEPKHEHKHEPKHEPH9HMEOKHEHKHEPKHEHKPEHKHEGH9HOKFEPKHEHKPEHKHEHKPEHKGH9HOEFKHEPKHEHKHEPKHEHKPEFH9HQKEEHKHEPKHEHKHEPKHEHKMH9HSKLEHKHEPKHEHKPEHKHEHKEH9HSEDKPEHKPEHKHEHKPEHKHEEH9HSKDEHKPEHKPKPEHKPEHKDH9HUECKHEHKPEHKHKXEH" -IM$(4) = "KPEDH9HUKCEHK5EHK5EHKLH9HUKKEHK5EHK5EHKCH9HWEBKPEHKXKHEHK8I9IWKZEHKPKPEHK0I9IWK7EHKHKXEHKSI9IWK9KFEHK9KOI9IWK9KNK9KOI9IWK9KNK9KOI9IWK9KNKPIHKZI9IYK6IHKHKXIHKSI9IWK9KFIHK5IHKKI9IWK9KNIHK5IHKCI9IYKPIHKXKHIHK5IHKPIHK5IHKPIHKPKPIHK0I9IWKJIHKPIH" -IM$(5) = "KHIHKPIHKPI9IZKBIHKHIHKPIHKHIHKPIHKKI9IWKJIHKHIHKPIHKHIHKPIHKDI9IXKPIHKHIHKHKHIHKHIPKHI9IYKCIHKPIHKHIHKPIHKHIPKHIHKPIHKHIPKHIHKPIHKHIHKPIHKHI9I9IHKHIHKPIHKHIPKHIHKHIEC9CSKDIPKHIPKHIHKHIPKHIHKEC9CSIDKHIPKHIPIPKHIPKHIFC9CQKEIHKHIPKHIHIXKHIPKG" -IM$(6) = "C9COIFKHI5KHI5KHIPKHI5KHIPKHI5KHI5KHIPKHI5KHIPKHIXIHKHI9IDC9CMI4KHIPIPKHI6C9CKI9IDKHIHIXKHIZC9CII9IMKHI9IWC9CGI9IVI9I9I9I9IPI9I9I9I9IPQ9Q9Q9Q9QPE9E9E9E9EPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9" -IM$(7) = "Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9" -IM$(8) = "Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP" -IM$(9) = "RBRQ9QOKMQHK5RBSQ9QTI9IRRBXQ9QTKPQHKHQPKERBYQ9QPI9IHRB3Q9QSKAI5KHIJRB5Q9QTC9CMRB9Q9QWI9IGRCBQ9QYC9CCRCIA9A9A9A9APRCJE9E9E9E9EPRCNA9A9A9A9APRCOE9E9E9E9EPRCUA9A9A9A9APRCVE9E9E9E9EPRC5A9A9A9A9APRC7E9E9E9E9EP" -IM$(10) = "RBQQ9QOK9KIQHKFRBRQ9QOI9IQRBWQ9QPKLQHKHQHKPRBXQ9QTI9QLIERB2Q9QRIBKHIPKHIPKCRB4Q9QSC9CORB8Q9QVI3KHIGRCAQ9QXC9CERCJA9A9A9A9APRCKE9E9E9E9EPRCOA9A9A9A9APRCPE9E9E9E9EPRCVA9A9A9A9APRCXE9E9E9E9EPRC7A9A9A9A9APRDAE9E9E9E9EP" -IM$(11) = "RBPQ9QOK9KAQHKNRBQQ9QOI9IWRBVQ9QOKEQHKHQHKPQHKFRBWQ9QPI9IPRB1Q9QQKCIPKHIPKHIDRB3Q9QSC9CORB7Q9QUIWKHIPRB9Q9QWC9CGRCEA9A9A9A9APRCFE9E9E9E9EPRCGA9A9A9A9APRCHE9E9E9E9EPRCPA9A9A9A9APRCQE9E9E9E9EPRCXA9A9A9A9APRCZE9E9E9E9EPRDAA9A9A9A9APRDCE9E9E9E9" -IM$(12) = "EP" -IM$(13) = "RBOQ9QNK3QHKWRBPQ9QOI9IWRBUQ9QTKHQHKPQHKNRBVQ9QOI9IWRB0Q9QQIKKHIPKHIHKDRB1Q9QQC9CSRB2Q9QRC9CQRB6Q9QTIPKHIYRB8Q9QVC9CIRCKA9A9A9A9APRCLE9E9E9E9EPRCQA9A9A9A9APRCRE9E9E9E9EPRCZA9A9A9A9APRC1E9E9E9E9EPRDCA9A9A9A9APRDFE9E9E9E9EP" -IM$(14) = "RBOQ9QNI9IYRBTQ9QOKEQHKPQHKVRBUQ9QTI9IRRBZQ9QTKHQHKPQHKHRB0Q9QQC9CSRB5Q9QTIHKHI5KARB7Q9QUC9CKRCBQ9QYI9ICRCHA9A9A9A9APRCIE9E9E9E9EPRCLA9A9A9A9APRCME9E9E9E9EPRCRA9A9A9A9APRCTE9E9E9E9EPRC1A9A9A9A9APRC3E9E9E9E9EPRDFA9A9A9A9APRDIE9E9E9E9EP" -IM$(15) = "RBSQ9QTKPQHK3RBTQ9QOI9IWRBYQ9QPKDQHKPQHKHRBZQ9QTI9ILRB4Q9QSIAKHI5KHIBRB6Q9QTC9CMRCAQ9QXI9IERCEE9E9E9E9EPRCFA9A9A9A9APRCGE9E9E9E9EPRCMA9A9A9A9APRCNE9E9E9E9EPRCTA9A9A9A9APRCUE9E9E9E9EPRC3A9A9A9A9APRC5E9E9E9E9EPRDIA9A9A9A9AP" +NS# = 32 +VA#(0) = 32 +VB#(0) = 35 +VA#(1) = 36 +VB#(1) = 39 +VA#(2) = 40 +VB#(2) = 43 +VA#(3) = 44 +VB#(3) = 47 +VA#(4) = 48 +VB#(4) = 51 +VA#(5) = 52 +VB#(5) = 55 +IM$(0) = "G9G9G9G9GPGHEHG9GTEHG9GTEHGPGPEHG9GTEHG9GTEHGHGXEHGTBAG8EHG9GTEH" +IM$(1) = "G5EHGPEHG5EHGPEHG5EHGAPAG3EHGPEHG5EHGPEHGXGHEHG5EHGPEHG5EHGFBAGI" +IM$(2) = "EHGPGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGBPAGMEHGPEHGHEH" +IM$(3) = "GPEHGHEHGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPEHGHEP" +IM$(4) = "GHEHGHEGBAEHGHEHGPEHGHEPGHEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPEC" +IM$(5) = "PAEDGHEPGHEHGHEPGHEPGHEHGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHECBAEDGHEP" +IM$(6) = "GHEPGHEHGHEPGHEHGHEPGHEPGHEHGHEPGHEPEPGHEPGHE5GHEPGHE5GHEHEXGHEP" +IM$(7) = "GHEEPAEZGHEPGHE5GHE5GHEPGHE5GHEPGHE5GHE9ETGHE9ETGHEXEHGHE9ETGHHA" +IM$(8) = "E9ESGHEPEPGBBAGEE9EMHOE9ETGHEHE9E7HUE9E6E9E4H0E9E3E9E3H2E9EHBAET" +IM$(9) = "EHKHE9ELH6E9ECKHEPEPKHE9EBH9HAE9EIKHEHEXKHE2H9HCE9EPKHE5KHEPKDH9" +IM$(10) = "HEKCEPKHE5KHE5KHEKH9HGEBKHEPKHEXEHKHE5KHEBH9HIEIKHEPKHEPEPKHEPKH" +IM$(11) = "EHKAH9HKKHEHKHEPKHEHKHEPKHEPKHH9HMEGKHEHKHEPKHEHKHEPKHEPH9HMEOKH" +IM$(12) = "EHKHEPKHEHKPEHKHEGH9HOKFEPKHEHKPEHKHEHKPEHKGH9HOEFKHEPKHEHKHEPKH" +IM$(13) = "EHKPEFH9HQKEEHKHEPKHEHKHEPKHEHKMH9HSKLEHKHEPKHEHKPEHKHEHKEH9HSED" +IM$(14) = "KPEHKPEHKHEHKPEHKHEEH9HSKDEHKPEHKPKPEHKPEHKDH9HUECKHEHKPEHKHKXEH" +IM$(15) = "KPEDH9HUKCEHK5EHK5EHKLH9HUKKEHK5EHK5EHKCH9HWEBKPEHKXKHEHK8I9IWKZ" +IM$(16) = "EHKPKPEHK0I9IWK7EHKHKXEHKSI9IWK9KFEHK9KOI9IWK9KNK9KOI9IWK9KNK9KO" +IM$(17) = "I9IWK9KNKPIHKZI9IYK6IHKHKXIHKSI9IWK9KFIHK5IHKKI9IWK9KNIHK5IHKCI9" +IM$(18) = "IYKPIHKXKHIHK5IHKPIHK5IHKPIHKPKPIHK0I9IWKJIHKPIHKHIHKPIHKPI9IZKB" +IM$(19) = "IHKHIHKPIHKHIHKPIHKKI9IWKJIHKHIHKPIHKHIHKPIHKDI9IXKPIHKHIHKHKHIH" +IM$(20) = "KHIPKHI9IYKCIHKPIHKHIHKPIHKHIPKHIHKPIHKHIPKHIHKPIHKHIHKPIHKHI9I9" +IM$(21) = "IHKHIHKPIHKHIPKHIHKHIEC9CSKDIPKHIPKHIHKHIPKHIHKEC9CSIDKHIPKHIPIP" +IM$(22) = "KHIPKHIFC9CQKEIHKHIPKHIHIXKHIPKGC9COIFKHI5KHI5KHIPKHI5KHIPKHI5KH" +IM$(23) = "I5KHIPKHI5KHIPKHIXIHKHI9IDC9CMI4KHIPIPKHI6C9CKI9IDKHIHIXKHIZC9CI" +IM$(24) = "I9IMKHI9IWC9CGI9IVI9I9I9I9IPI9I9I9I9IPQ9Q9Q9Q9QPE9E9E9E9EPE9E9E9" +IM$(25) = "E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QP" +IM$(26) = "Q9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9" +IM$(27) = "Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9" +IM$(28) = "QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9" +IM$(29) = "Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9" +IM$(30) = "Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP" +IM$(31) = "Q9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP" +IM$(32) = "RBRQ9QOKMQHK5RBSQ9QTI9IRRBXQ9QTKPQHKHQPKERBYQ9QPI9IHRB3Q9QSKAI5" +IM$(33) = "KHIJRB5Q9QTC9CMRB9Q9QWI9IGRCBQ9QYC9CCRCIA9A9A9A9APRCJE9E9E9E9EP" +IM$(34) = "RCNA9A9A9A9APRCOE9E9E9E9EPRCUA9A9A9A9APRCVE9E9E9E9EPRC5A9A9A9A9" +IM$(35) = "APRC7E9E9E9E9EP" +IM$(36) = "RBQQ9QOK9KIQHKFRBRQ9QOI9IQRBWQ9QPKLQHKHQHKPRBXQ9QTI9QLIERB2Q9QR" +IM$(37) = "IBKHIPKHIPKCRB4Q9QSC9CORB8Q9QVI3KHIGRCAQ9QXC9CERCJA9A9A9A9APRCK" +IM$(38) = "E9E9E9E9EPRCOA9A9A9A9APRCPE9E9E9E9EPRCVA9A9A9A9APRCXE9E9E9E9EP" +IM$(39) = "RC7A9A9A9A9APRDAE9E9E9E9EP" +IM$(40) = "RBPQ9QOK9KAQHKNRBQQ9QOI9IWRBVQ9QOKEQHKHQHKPQHKFRBWQ9QPI9IPRB1Q9" +IM$(41) = "QQKCIPKHIPKHIDRB3Q9QSC9CORB7Q9QUIWKHIPRB9Q9QWC9CGRCEA9A9A9A9AP" +IM$(42) = "RCFE9E9E9E9EPRCGA9A9A9A9APRCHE9E9E9E9EPRCPA9A9A9A9APRCQE9E9E9E9" +IM$(43) = "EPRCXA9A9A9A9APRCZE9E9E9E9EPRDAA9A9A9A9APRDCE9E9E9E9EP" +IM$(44) = "RBOQ9QNK3QHKWRBPQ9QOI9IWRBUQ9QTKHQHKPQHKNRBVQ9QOI9IWRB0Q9QQIKKH" +IM$(45) = "IPKHIHKDRB1Q9QQC9CSRB2Q9QRC9CQRB6Q9QTIPKHIYRB8Q9QVC9CIRCKA9A9A9" +IM$(46) = "A9APRCLE9E9E9E9EPRCQA9A9A9A9APRCRE9E9E9E9EPRCZA9A9A9A9APRC1E9E9" +IM$(47) = "E9E9EPRDCA9A9A9A9APRDFE9E9E9E9EP" +IM$(48) = "RBOQ9QNI9IYRBTQ9QOKEQHKPQHKVRBUQ9QTI9IRRBZQ9QTKHQHKPQHKHRB0Q9QQ" +IM$(49) = "C9CSRB5Q9QTIHKHI5KARB7Q9QUC9CKRCBQ9QYI9ICRCHA9A9A9A9APRCIE9E9E9" +IM$(50) = "E9EPRCLA9A9A9A9APRCME9E9E9E9EPRCRA9A9A9A9APRCTE9E9E9E9EPRC1A9A9" +IM$(51) = "A9A9APRC3E9E9E9E9EPRDFA9A9A9A9APRDIE9E9E9E9EP" +IM$(52) = "RBSQ9QTKPQHK3RBTQ9QOI9IWRBYQ9QPKDQHKPQHKHRBZQ9QTI9ILRB4Q9QSIAKH" +IM$(53) = "I5KHIBRB6Q9QTC9CMRCAQ9QXI9IERCEE9E9E9E9EPRCFA9A9A9A9APRCGE9E9E9" +IM$(54) = "E9EPRCMA9A9A9A9APRCNE9E9E9E9EPRCTA9A9A9A9APRCUE9E9E9E9EPRC3A9A9" +IM$(55) = "A9A9APRC5E9E9E9E9EPRDIA9A9A9A9AP" REM ---- PICTURE-END SS# = 0 SE# = NS# - 1 @@ -1317,13 +1357,25 @@ TB# = TB# + 1 IF TB# > 2 THEN TB# = 0 RETURN +REM A batch is four PLAY statements, one bar each: the parser's V, T, +REM U and duration state persists across statements and every PLAY +REM appends to the same queue, so four bars queue exactly as one long +REM string would -- which the 80-column line limit no longer allows. +REM Each bar restates the prefix anyway, so a bar dropped by QFULL +REM never leaves the next one playing with drum-kit state. LABEL TUNEA -PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CO4AE O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A O1CO2CO3CO2CO3CEGO4CO1CO2CO3EGO4CEGO5C O1GO2GO3GO2GO3GBO4DGO1GO2GO3BO4DGBO5DO4B" +PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CO4AE" +PLAY "V1T3U9S O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A" +PLAY "V1T3U9S O1CO2CO3CO2CO3CEGO4CO1CO2CO3EGO4CEGO5C" +PLAY "V1T3U9S O1GO2GO3GO2GO3GBO4DGO1GO2GO3BO4DGBO5DO4B" MT# = TI# + 270 RETURN LABEL TUNEB -PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CEO4A O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A O1DO2DO3DO2DO3DFAO4DO1DO2DO3FAO4DFAO5D O1EO2EO3EO2EO3E#GBO4EO1EO2EO3#GBO4E#GBO5E" +PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CEO4A" +PLAY "V1T3U9S O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A" +PLAY "V1T3U9S O1DO2DO3DO2DO3DFAO4DO1DO2DO3FAO4DFAO5D" +PLAY "V1T3U9S O1EO2EO3EO2EO3E#GBO4EO1EO2EO3#GBO4E#GBO5E" MT# = TI# + 270 RETURN diff --git a/examples/megademo/vaporwave.py b/examples/megademo/vaporwave.py index 8f5b73f..4103aa1 100644 --- a/examples/megademo/vaporwave.py +++ b/examples/megademo/vaporwave.py @@ -58,7 +58,14 @@ SKIP = "Q" ROWREC = "R" LENCH = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" MAXRUN = len(LENCH) -PAYLOAD = 240 + +# The interpreter reads source through an 80-byte line buffer and refuses +# any line that fills it (AKBASIC_MAX_LINE_LENGTH, sink_stdio.c), so a +# stored line is at most 78 characters plus its newline. 'IM$(NN) = "' and +# the closing quote spend 12 of those; 64 keeps the emitted lines under +# the ceiling with margin to spare while the index stays two digits. +PAYLOAD = 64 +MAXLINE = 78 SUN_CX, SUN_CY, SUN_R = 80, 50, 30 HORIZON = 74 @@ -206,13 +213,32 @@ def encode_delta(prev, cur): def chop(blob): - return [blob[i:i + PAYLOAD] for i in range(0, len(blob), PAYLOAD)] + """Split a stream into strings of at most PAYLOAD characters, cutting + only between records. The decoder reads a record's tail characters + with MID on the string it is walking, so a run (two characters) or a + row record (three) that straddled two IM$ entries would decode as + garbage; DRAWSTREAM only carries the cursor, never a partial record.""" + out, cur = [], "" + p = 0 + while p < len(blob): + n = 3 if blob[p] == ROWREC else 2 + if len(cur) + n > PAYLOAD: + out.append(cur) + cur = "" + cur += blob[p:p + n] + p += n + if cur: + out.append(cur) + return out -def simulate(raster, blob): +def simulate(raster, blob, x=0, y=0): """Apply one encoded stream to a raster exactly the way the BASIC - decoder does, skips-draw-nothing and all.""" - x = y = p = 0 + decoder does, skips-draw-nothing and all. The cursor comes in and + goes back out because DRAWSTREAM carries it from one IM$ entry to + the next -- decoding the chopped strings one at a time with the + cursor threaded through is exactly what the demo will execute.""" + p = 0 while p < len(blob): c = blob[p] if c == ROWREC: @@ -230,20 +256,31 @@ def simulate(raster, blob): x = 0 y += 1 p += 2 + return raster, x, y + + +def simulate_lines(raster, lines): + """One stream as its chopped strings, cursor carried across the + boundaries the way DRAWSTREAM carries X# and Y#.""" + x = y = 0 + for line in lines: + raster, x, y = simulate(raster, line, x, y) return raster -def verify(frames, base_blob, delta_blobs): +def verify(frames, base_lines, delta_line_groups): """The base must reproduce frame 0 exactly, and each delta must carry the raster exactly to the next frame. A skip leaves the cell the encoder promised was already right, so equality is total and - any difference at all is an encoder bug.""" + any difference at all is an encoder bug. This decodes the CHOPPED + strings, not the blobs, so a chop that split a record would fail + here instead of corrupting the screen.""" raster = [[1] * W for _ in range(H)] - raster = simulate(raster, base_blob) + raster = simulate_lines(raster, base_lines) assert raster == frames[0], "base stream does not reproduce frame 0" - for i, blob in enumerate(delta_blobs): + for i, lines in enumerate(delta_line_groups): want = frames[(i + 1) % PHASES] - raster = simulate(raster, blob) + raster = simulate_lines(raster, lines) assert raster == want, "delta %d does not reproduce its frame" % i @@ -261,6 +298,9 @@ def emit_block(base_lines, delta_ranges, all_lines): for i, s in enumerate(all_lines): out.append('IM$(%d) = "%s"' % (i, s)) out.append("REM ---- PICTURE-END") + for line in out: + assert len(line) <= MAXLINE, "emitted line over %d chars: %r" % ( + MAXLINE, line) return out @@ -303,15 +343,15 @@ def main(): base_lines = chop(base_blob) all_lines = list(base_lines) delta_ranges = [] - delta_blobs = [] + delta_line_groups = [] for i in range(PHASES): blob = encode_delta(frames[i], frames[(i + 1) % PHASES]) - delta_blobs.append(blob) lines = chop(blob) + delta_line_groups.append(lines) delta_ranges.append((len(all_lines), len(all_lines) + len(lines) - 1)) all_lines.extend(lines) - verify(frames, base_blob, delta_blobs) - dbytes = sum(len(b) for b in delta_blobs) + verify(frames, base_lines, delta_line_groups) + dbytes = sum(len(l) for g in delta_line_groups for l in g) print("base %d bytes in %d strings; video %d bytes in %d strings; " "%d strings total" % (sum(len(s) for s in base_lines), len(base_lines), dbytes,