Log In  

Cart #cursedconsole-0 | 2021-09-29 | Code ▽ | Embed ▽ | Forks ▽ | No License
9

Plot

Oh mighty @zep, a quest for thee: to fix this bug in oh-two-three.
Time's very fabric has been torn, and monsters from the past reborn!
A pumpkin army's impossible roll befouls our once cozy console.

Its humbled clock refunds too much, permitting Cauldron's villain's touch.
Relentlessly they spin and taunt, they dare thee to remove their haunt.
Please help us take our console back and save us from their cycle hack!

Their jeering eyes provide the clue, all stars show what you must do.
A table's count below sixteen, as low as any number's been,
Negate it and they'll mock you more... the wellspring of this wretched flaw!

About

An entry in 1023 chars to the #pico1k jam. See itchio page for the cart frozen using PICO-8 0.2.3, in case zep completes his quest and this no longer works in later versions of the console. ;)

The cart demonstrates a virtual CPU exploit in PICO-8 v0.2.3, as ordinarily the fantasy console would not be fast enough to do the rendering at 30fps letalone 60fps. Since we're breaking out of the virtual machine limit the speed will depend on your device. The standalone PICO-8 app runs at 60fps on my dekstop, but the web player is smoother at 30fps. Note the cart played above is a slightly earlier version in 1019 chars which runs at 30fps.

Before we delve into the deciphered demo, feast your eyes on this grisly gist of the final code. You should be able to copy and paste this into your rebooted console and run it. Note I had to rewrite a tab character in one of the strings as \t to get around the lexaloffle website mangling it.

1023 Character Crafty Code

_set_fps(60)๐˜ฉ=64โ˜…={}s=.001๐˜ด=sin
while(#โ˜…>=0)add(โ˜…,โ˜…)
poke(24337,ord('โ–‘โด์›ƒ\t\n♥โฌ‡๏ธโฌ‡๏ธ³³โฌ…๏ธโŒ‚\0\0',1,14))๐˜ข=abs
fillp(โ–’\1)๐˜ค=cos?"โถc0โถwโถt  แถœeโ˜…"
function ๐˜ญ(๐˜บ)all(โ˜…)for t=0,โ–ˆ,s do
w=r-r/3*๐˜ด(t*7%โ–ˆ)x=w*๐˜ค(t)๐˜น=๐˜ข(x)z=w*๐˜ด(t)l=(x*x+y*y+z*z)^.5b=1+๐˜ข(x/5+y/3+z)/l*5
if(y>=11-x^2/200and y<=27-x^2/83+๐˜น*.1or pget(๐˜น,-y)>0)b=8
sset(๐˜ฉ+x,๐˜บ+y,b+((b+โ–ˆ)\1-b\1)*8)end end
y=0while(y<9)r=2+y/16๐˜ญ(22)y+=1
a=0while(a<.43)d=30+15*๐˜ข(๐˜ด(a))y=d*๐˜ค(a)r=d*๐˜ด(a)๐˜ญ(๐˜ฉ)a+=s
function p(v)poke2(d,v)d+=๐˜ฅ end
๐˜ฅ=2๐˜ฎ=12280d=๐˜ฎ for i=1,84do n=ord('@/\0ใ€@)t๐˜ธแถœแถœใ‚‰แถ แถ แต‰แต‰โ˜โ˜ใโ™โ™ใโ—โ—โ– โ– ใ‚‰โ—โฌ…๏ธแต‰แต‰…ใ‚จใ›ใ‚‰ใ‚ใฌ$+07<70+ใฏ$,08<80,ใƒฃโ—ใป$)05<50)ใฏ$*06<60*&+2โ—†72โ—†ใ›*ใƒฃใฏ',i)
if(n<128)p(n)d-=1else for _=0,n\16%8do p(%(d-n%16*2-2))end end
p(-32384)p(770)
d+=316๐˜ฅ=68v=7169p(v+24)p(v+32)p(v)p(v)music()
๐˜ณ=0function ๐˜ฑ()p(%[email protected])c+=2end
::_::?"โถ1"
while(๐˜ณ-8&31!=stat(20))d=12800+๐˜ณ%32*2a=๐˜ณ%128+๐˜ฎ+8c=๐˜ฎ ๐˜ฑ()๐˜ฑ()a+=128๐˜ฑ()๐˜ฑ()๐˜ณ+=1
a=t()*.1c=๐˜ค(a)s=๐˜ด(a)๐˜ป=s+2for y=-๐˜ฉ,๐˜ฉ do all(โ˜…)for x=-๐˜ฉ,๐˜ฉ do v=๐˜ฉ+(s*x+c*y)*๐˜ป&127
๐˜ท=v-73if(๐˜ท>0)v-=๐˜ท*๐˜ด(a*16)/2
k=sget(๐˜ฉ+(c*x-s*y)*๐˜ป&127,v)d=k\8k%=8if(k>0and v<30)k+=7
pset(๐˜ฉ+x,๐˜ฉ+y,k+(k+d)*16)end end goto _

Scintillating Source

Now let's rename the identifiers, remove variables that exist only to reduce code size, convert the strings to bytes, expand some shorthand ifs, whiles and prints, unfold some constants, and format it legibly to explain what it's doing. Once again you should be able to copy and paste this into your console and run it.

-- The standalone PICO-8 app may handle 60fps, depending on your device.
-- Remove this to run at 30fps.
_set_fps(60)

-- The "all star" virtual CPU exploit!
--
-- PICO-8's all(c) function very nicely refunds some virtual cpu cycles via 
-- "_refund_cpu_((#c >= 16) and -16 or -#c", where _refund_cpu is an
-- internal function that actually adds to the virtual cycles consumed;
-- so a negative argument will refund cycles (cart goes faster), 
-- and a positive argument will consume cycles (cart goes slower).
--
-- all(c) where #c==0 causes no extra cycles to be refunded or consumed.
-- all(c) where #c>0 and #c<16 results in #c cycles being refunded.
-- all(c) where #c>=16 results in only 16 cycles being refunded.
--
-- To exploit _refund_cpu and achieve undeserved cycles refunded we
-- need #c to be less than 16 but -#c to be negative and large.
--
-- Negative numbers don't seem to help, even if we could make a negative
-- sized table. -#c will just be positive so _refund_cpu will actually 
-- consume extra cycles and the cart will go slower!
--
-- Fortunately for us, in 1945 John von Neumann anticipated humanity's
-- need to exploit fantasy consoles 76 years hence and prankishly proposed
-- using twos complement to represent negative numbers in future so-
-- called "electronic stored-program digital computers".
--
-- In PICO-8, 0x8000 == 32768 == -32768 == -0x8000. So if we can make
-- #c==-32768 then all(c) will call _refund_cpu(-#c == -32768) and John's 
-- prescient plan will reach its triumphant culmination.
--
-- In lua the __len operation on a table's metatable will be called by #c.
-- So we could do:
--
-- c={__len=function() return -32768 end}
-- setmetatable(c,c)
--
-- but we can do it in fewer characters if we have plenty of memory:
star={}
while(#star>=0)add(star,star)
-- since 32767+1 == -32768 this loops until #star == -32768.
-- now all(star) will call _refund_cpu(-32768) and time will flow backwards!
--
-- Note: it doesn't matter what we add to the table but add (โ˜…,โ˜…) looks
-- spookily like a malevolent pumpkin staring hungrily through the depths of 
-- time...

-- Set the screen palette, equivalent to pal({...},1) but shorter.
poke(0x5f11,ord("\x84\x04\x89\x09\x0a\x87\x83\x83\x03\x03\x8b\x8a\x00\x00",1,14))

-- The cart's palette is:
-- Color 0 is unassigned as it defaults to zero (black), as desired.
-- Colors 1-6 define a brown/orange/yellow gradient for the pumpkin's skin.
-- Colors 7-12 define a green gradient for the pumpkin's stalk.
-- Color 13 is black because the approximate math behind the pumpkin actually
-- generates a 7th gradient color in the top-middle pixel of the stalk,
-- and setting that to black results in a pleasing "fork" in the stalk, if
-- you look closely and squint a bit.
-- Color 14 is black because PICO-8's default spritesheet has a star drawn
-- in color 7 in the top-left; later we'll see we add 7 to the spritesheet
-- color near the top of the spritesheet, and setting color 14 to black was
-- cheaper than clearing the unwanted pixels.
-- (In retrospect we could have shifted the palette entries up one index
-- and had the gradients start at color 2, to avoid the duplicate blacks
-- and thereby saved two chars).

-- In PICO-8 โ–’ is a variable set to 0b0101101001011010.1 which is a checkered
-- fill pattern. We need to remove the .1 transparency bit using \1.
fillp(โ–’\1)

-- P8SCII code to clear the screen, set wide mode on, set tall mode on, skip
-- a couple spaces, use color 14 (black but non-zero) and draw a star. We will
-- pget these pixels later to cheaply carve the pumpkin's eyes (upside-down).
print("โถc0โถwโถt  แถœeโ˜…")

-- plot_pumpkin_row draws one row of either the pumpkin face or stalk to the
-- spritesheet. We draw the pumpkin to the spritesheet once on startup, then
-- each frame read from the spritesheet to render it to the screen.
-- We iterate angles to derive individual 3d pixel positions on the surface,
-- then completely fake lighting in a mathematically unsound yet surprisingly
-- pleasing way.
-- The resulting pixel is assigned a pair of colors to represent a smoother
-- gradient via the fill pattern. So in fact there are 11 "orange" values 
-- counting in-between colors, plus one for the mouth/eyes, and 12 "green" 
-- values.
-- PICO-8 only stores 4 bits per pixel in the spritesheet, so we actually only
-- store the uncolored lighting value which happens to coincide with the
-- orange gradient colors.
-- When rendering the pumpkin each frame, we check each pixel's y coordinate
-- to determine whether it was a stalk, then shift the color to the green 
-- gradient by adding 7. (This is why that wayward star in the default 
-- spritesheet ends up with color 14, which is why we set index 14 to black.)
function plot_pumpkin_row(yofs)
 -- exploit: accelerate this slow startup code!
 all(star)
 for t=0,.5,.001 do
  -- https://www.shadertoy.com/view/4tBcRV is a great demonstration of this
  -- "pumpkin segment" math to get the distance to the surface from the
  -- vertical axis.
  w=r-r/3*sin(t*7%.5)
  -- x is screen position relative to the origin (middle of the screen)
  x=w*cos(t)
  absx=abs(x)
  -- z is distance from XY plane through pumpkin's center
  z=w*sin(t)
  -- Fake diffuse lighting using surface position as normal and an
  -- unnormalized light vector. The value is put in the range 1-6.
  l=(x*x+y*y+z*z)^.5
  b=1+abs(x/5+y/3+z)/l*5
  -- Check if this pixel is part of the mouth (first expression) or
  -- the eyes (second expression) by reading the star we printed earlier.
  if (y>=11-x^2/200 and y<=27-x^2/83+absx*.1) or (pget(absx,-y)>0) then
   b=8 -- base color is 0, and the high bit=8 indicates a 2-color pattern
  end
  -- If the pixel value's fractional part is >= 0.5, then set the high bit=8
  -- to remember to use a two color gradient.
  sset(64+x,yofs+y,b+((b+.5)\1-b\1)*8)
 end
end
-- Plot the pumpkin stalk to the spritesheet
y=0
while y<9 do
 -- Stalk is thicker at the base
 r=2+y/16
 -- Plot one row of the stalk
 plot_pumpkin_row(22)
 y+=1
end
-- Plot the pumpkin face to the spritesheet
a=0
while a<.43 do
 -- Distance to each horizontal slice
 d=30+15*abs(sin(a))
 -- y coordinate of slice, relative to the screen origin
 y=d*cos(a)
 -- Horizontal radius of slice
 r=d*sin(a)
 -- Plot one row of the face
 plot_pumpkin_row(64)
 a+=.001
end

-- Utility function to poke2 a value to the global 'dst' and advance dst 
-- by 'dststep'. Reused with two different dststep values.
function poke2_step(v)
 poke2(dst,v)
 dst+=dststep
end

-- Uncompress the music data
--
-- The uncompressed music data has some per-channel values (4 words), plus two
-- sequences of 128 note pitches (256 bytes). 
-- The per-channel word values define the SFX instrument, volume and effect. 
-- Each note's pitch is added to this value when it's written to the SFX.
-- Channel 0 is 0x2f40: organ, volume 7, vibrato. (sequence A)
-- Channel 1 is 0x1900: pulse, volume 4, slide. (sequence A)
-- Channel 2 is 0x2940: organ, volume 4, vibrato. (sequence B)
-- Channel 3 is 0x5774: noise, volume 3, fade out. (sequence B, pitch offset -12)
--
-- We use simple LZ style compression customized for the input. All the pitches
-- are in the range 0-63, and serendipitously the per-channel words above also
-- only have bytes less than 0x80, so we use bit 7 to mark offset/length pairs.
-- It turns out the sequences have a lot of repetition, and limiting the offset
-- to multiples of 2 between 2-32 and length to multiples of 2 between 2-16
-- works well and packs into 8 bits.
--
-- The sound data is poked to just before 0x3100 so that the dst pointer is
-- ready to setup music pattern data after uncompressing.
--
-- Counting the compressed pitches plus uncompression code, 177 chars are 
-- used in the final cart, compared to 256 bytes for the raw pitches.
snddata=0x3100-(256+8)
dststep=2
dst=snddata
-- Iterate compressed music data bytes
for i=1,84 do
 n=ord("\x40\x2f\x00\x19\x40\x29\x74\x57\x0c\x0c\xc0\x0f\x0f\x0e\x0e\x14\x14\xa0\x13\x13\xa0\xff\xff\x11\x11\xc0\x86\x8b\x0e\x0e\x90\xcf\xa7\xc0\xbb\xb0\x24\x2b\x30\x37\x3c\x37\x30\x2b\xb3\x24\x2c\x30\x38\x3c\x38\x30\x2c\xfb\xff\xb7\x24\x29\x30\x35\x3c\x35\x30\x29\xb3\x24\x2a\x30\x36\x3c\x36\x30\x2a\x26\x2b\x32\x8f\x37\x32\x8f\xa7\x2a\xfb\xb3",i)
 if n<128 then
  -- This is a payload byte, poke it
  poke2_step(n)
  -- Our utility function writes words, so step back a byte
  dst-=1
 else
  -- This is an offset/length pair; copy from prior uncompressed data, word by word.
  -- (Allows reading words just copied to get repeated subsequences)
  for _=0,n\16%8 do
   poke2_step(%(dst-n%16*2-2))
  end 
 end
end
-- Poke the pattern data; we set up a single pattern that references SFX 0,1,2,3 and loops.
poke2_step(0x8180)
poke2_step(0x0302)
-- Skip dst to 0x3200+64 where we will poke SFX control data
dst+=316
-- Each SFX is 68 bytes
dststep=68
v=0x1c01
-- Channel 0's SFX control is 0x1c19: speed=28, reverb=1
-- Channel 1's SFX control is 0x1c21: speed=28, reverb=1, detune=1
-- Channel 2's SFX control is 0x1c01: speed=28
-- Channel 3's SFX control is 0x1c01: speed=28
poke2_step(v+0x18)
poke2_step(v+0x20)
poke2_step(v)
poke2_step(v)
-- Same as music(0); starts the music
music()

-- sndrow tracks which music row we're about to stream out.
-- sndrow%128 is the note index to read from the music data.
-- sndrow%32 is the SFX row to poke.
sndrow=0

-- poke_note_step is a utility function which reads the per-
-- channel word value, and adds it to the current sequence's
-- pitch value, then writes it to the SFX.
function poke_note_step()
 -- Note: dststep is still 68, so this will advance the dst
 -- pointer to the next SFX.
 -- Add word per-channel value to byte sequence pitch.
 poke2_step(%[email protected])
 -- Advance to the next per-channel word.
 c+=2
end

::mainloop::
 -- Flip
 print("โถ1")
 -- Keep 8 notes ahead of the current music playback row streamed
 -- to SFX 0-3. (If doing dynamic gameplay sounds reduce this to 2)
 -- Note: I tried instead setting up 16 SFX to contain the entire song
 -- but that used more chars than the streaming version. Streaming
 -- would also allow dynamic music - with a few more chars we could
 -- have the channels fade in one at a time like the C64 version of
 -- Cauldron...
 while sndrow-8&31 != stat(20) do
  -- Setup dst to channel 0's (i.e. SFX 0's) current streaming row.
  dst=0x3200 + sndrow%32*2
  -- Address of sequence A's streaming pitch
  a=sndrow%128 + snddata + 8
  -- Address of channel 0's per-channel values
  c=snddata
  -- Channel 0 and 1 share sequence A's pitches
  poke_note_step()
  poke_note_step()
  a+=128
  -- Channel 2 and 3 share sequence B's pitches
  poke_note_step()
  poke_note_step()
  sndrow+=1
 end

 -- Render the pumpkin, pixel by pixel
 a=t()*.1
 c=cos(a)
 s=sin(a)
 zoom=s+2
 -- For each y coord relative to the middle of the screen
 for y=-64,64 do
  -- Exploit: accelerate this slow loop!
  all(star)
  -- For each x coord relative to the middle of the screen
  for x=-64,64 do 
   -- Rotzoom screen x,y to get spritesheet coords u,v
   u=64+(c*x-s*y)*zoom & 127
   v=64+(s*x+c*y)*zoom & 127
   -- Animate the pumpkin's jaw up and down
   jawv=v-73
   if jawv>0 then
    v-=jawv*sin(a*16)/2
   end
   -- Get spritesheet pixel and separate into base color value 'b'
   -- and dither flag 'd'
   b=sget(u,v)
   d=b\8
   b%=8
   -- Recolor the stalk from shades of orange to shades of green.
   -- Note: The stalk actually should start at v=30, but leaving that
   -- row orange fakes a little perspective on top of the center slice
   -- of the pumpkin face!
   -- Note: This is also where that unwanted default 'x' in the 
   -- spritesheet gets recolored to color 14, which we set to black in
   -- the palette to hide it. It's drawn every frame!
   if (b>0 and v<30) then
    b+=7
   end
   -- Plot the pixel on the screen. Note the second color for the dither
   -- pattern goes into the high nibble.
   pset(64+x,64+y,b+(b+d)*16)
  end
 end 
goto mainloop
P#97954 2021-09-29 02:15 ( Edited 2021-09-30 21:03)

I hope the low stars count is just because maybe this got overlooked being in the jam category instead of releases because this is: 1 Incredible coding, 2 Great learning material thanks to the detailed commenting. Really thanks a lot

P#100099 2021-11-14 03:14
:: dw817

Runs badly in Firefox, @TheRoboZ. And @carlc27843, this is not your fault. It runs great in the main Pico-8 system. I like it. Star work. It's just something for @zep to explore - as you mentioned.

And yes it is possible that some people are not rating this because it runs so poorly in Firefox without reading the details.

P#100100 2021-11-14 03:41 ( Edited 2021-11-14 03:44)

[Please log in to post a comment]

Follow Lexaloffle:        
Generated 2022-10-04 17:17:39 | 0.027s | Q:17