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,