Log In  


first off, a demo. here's a cart that stores the entirety of its game logic in its sprite sheet:

Cart #baloonbomber_rom-0 | 2023-10-06 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
20

this is a parens-8 port of an old cart of mine. it has the lua code for the parens-8 language, and then parens-8 code for loading parens-8 game logic from ROM. you can find the full source for the game logic here

parens-8 is a lisp interpreter/compiler designed specifically to bypass the lua token limit.
the idea: use a portion of your cart (between 330 and 900 tokens, depending on your use case) to store the parens-8 interpreter, offload performance noncritical code into strings, and then optionally into ROM.

the parens-8 github repo has detailed instructions on how to achieve this, and how to customize parens-8 to best fit your project.

let's look at an excerpt of the original lua code for baloonbomber:

function make_explosion( x, y, r )
	local explosion = { x = x, y = y, r = r, ttl = 10, fg = true }
	function explosion:update( )
		if self.ttl < 5 then
			self.x -= speed
			make_smoke(self.x + rnd(self.r) - self.r / 2,
			           self.y + rnd(self.r) - self.r / 2)
		end
	end
	function explosion:draw( )
		circfill(self.x, self.y, self.r * min(self.ttl / 8, 1),
		         explosion_colors[11 - self.ttl] or 0)
	end
	add_particle(explosion)
end

and here is the corresponding parens-8 implementation:

(set make_explosion (fn (x y r) (add particles (zip
    (split "x,y,r,ttl,fg,update,draw") (pack
        x y r 10 1
        (fn (self) (env self (when (< ttl 5) (id
            (set x (- x speed))
            (make_smoke (- (+ x (rnd r)) (/ r 2))
                        (- (+ y (rnd r)) (/ r 2)))))))
        (fn (self) (env self
            (circfill
                x y
                (* r (min (/ ttl 8) 1))
                (or ([] explosion_colors (- 11 ttl)) 0)))))))))

the syntax may seem hostile, but the semantics are very much similar to that of lua. you may even identify common pico-8 patterns like using split or remapping _ENV.

parens-8 comes with a pretty significant performance overhead (see github repo for benchmarks), so while calls to the pico-8 api and lua functions are unaffected, tight loops and the like are dramatically slower in parens-8. make sure you minimize the cpu time spent in parens-8 code.

things you could probably offload as parens-8 code:

  • your _init, _update and _draw functions
  • complicated actor behavior where different actors have very different logic (coroutines are supported!)
  • weird glue code that pokes a bunch of flags, sets up pal calls, etc

things you definitely shouldn't write in parens-8:

  • your particle system
  • your rendering routines
  • 3d rotation matrix computations

baloonbomber - parens-8 edition is the answer to the question "can I offload my entire game to ROM":
yes, but you probably shouldn't.

20


1

parens-8 was updated with an optimized parser! token count is now:

  • 337 for the interpreter (was 402)
  • 375 for the compiler (was 440)

the updated library can be found on the parens-8 github


I don't understand it very well. Can you give me a tutorial? In this case, it's already compiled, right?


1

this is something very advanced!

there are two modes: interpreted and compiled

  • interpreted means that you save a lot of tokens, because instead of normal lua code you have one big string of lisp that you pass to the parens8 eval function
  • compiled goes even further and saves lots of characters: the lisp code is [wrong, see message below] compiled into bytes that are saved in the cart ROM (instead of sfx for example, or instead of map data) and loaded to be executed

more details are in the readme of the project

if you wonder whether you should use this, then you don’t! I think this is partly a fun experiment for the creator, partly a real useful tool for people who have made multiple games and are bumping on the pico8 limits and really want to stay with pico8

these articles are very interesting as background:


1

so, interpreted vs compiled in this case has nothing to do with bytecode vs text, neither of them use bytecode.

compiled in this case refers to the way the code is evaluated:

  • in interpreted parens-8, the syntax tree is parsed during execution, as different parts of the code are reached.
  • in compiled parens-8, the entire syntax tree is parsed once in the parens8(code) call, and turned into a large chain of lambdas. this means that after a parens8 call that defines a function, the code inside the function doesn't have to do any parsing work anymore, unlike the interpreted equivalent.

in short: "compiled" parens-8 is faster but heavier than interpreted parens-8, both in tokens and in memory usage. it's a tradeoff you can make a choice on.

there are still parts of language that might be improved: currently, scope lookup is still dynamic, and I might be able to improve performance a fair bit by compiling that scope lookup the same way I compile the AST. doing so introduces a few challenges that I haven't been able to overcome yet. most compiler books out there aren't particularly worried about code size in tokens, or doing silly things like running an interpreter inside another interpreter.


I guess I was misled by the example code that had a function to load from ROM! I thought it was bytecode saved there. what is it actually?


it's just plain ASCII text (as in, source code). you could probably compress it very effectively (and write the inflater in parens-8).



[Please log in to post a comment]