Choice 8: choicescript runtime on the pico 8

Sorry if this is the wrong place but wanted to share a weekend project: running the default choice script demo on the pico 8. i made a transpiler that converts choicescript scenes to a pico cart and embeds a custom cs runtime (only supports a handful of cs commands and doesn’t offer full compatiblity). below are some photos of it running and heres the pico8 cart if anyone wants to test it: Pico8

Note: there are some bugs but overall runs to the end of the demo

edit: for those curious about what cs commands are supported those are: choice, goto, label, finish, set, gosub, return, page_break, if, text, goto_scene, create.

7 Likes

For controls they are: X to continue/select choice and arrow keys to switch between choices

Pico8 is heavily restrained by design, for example just this demo is 2510 tokens and pico8’s limit is 8192 tokens. Now there’s some neat hacks to work around it (storing text in image and sound data sections of the pico8 cart) but the most anyone has done is store 2.5 chapters of moby dick which is roughly 3-5k words at best so no full game is doable on the pico8

2 Likes

Hey this is cool! Really speaks to the persistent flexibility of choicescript, PICO and classic web stacks. I personally also am a sucker for a minimalist aesthetic.

1 Like

decided to share the cs lua runtime:

RUNTIME_LUA = r'''-- cs2p8: choicescript runtime for pico-8

o_txt,o_ch,o_gt,o_lb,o_gs,o_fn,o_pb,o_st,o_if,o_cr,o_gsb,o_rt,o_end,o_lk=
  0,   1,   2,   3,   4,   5,   6,   7,   8,   9,   10,  11,  12,  13

v={}           
sl={}        
si=1           

es={}         
rs={}          
function cur_lvl() return es[#es] end
function sc() return cur_lvl()[1] end
function ci() return cur_lvl()[2] end
function lb() return cur_lvl()[3] end

-- display state
st="text"      
buf={}         
bl=1          
ch_opts=nil    
ch_sel=1      
maxl=16       

ww=30

function mklb(insts)
  local l={}
  if insts then
    for i=1,#insts do
      if insts[i][1]==o_lb then
        l[insts[i][2]]=i
      end
    end
  end
  return l
end

function pushlvl(insts)

  if #es>0 then
    cur_lvl()[2]=ci()
  end
  add(es,{insts,1,mklb(insts)})
end

function poplvl()
  if #es>1 then
    es[#es]=nil
  end
end


function findlb(name)
  for i=#es,1,-1 do
    local idx=es[i][3][name]
    if idx then return i,idx end
  end
  return nil,nil
end

function _init()
  sl=scene_list
  si=1
  load_scene(sl[si])
end

function load_scene(name)
  local insts=s[name]
  es={}
  rs={}
  buf={}
  bl=1
  st="text"
  ch_opts=nil
  pushlvl(insts)
  step()
end

function getv(name)
  return v[name] or 0
end

function setv(name,val)
  v[name]=val
end


function isalnum(c)
  return (c>="a" and c<="z") or (c>="A" and c<="Z") or (c>="0" and c<="9") or c=="_"
end

function isopchar(c)
  return c=="<" or c==">" or c=="=" or c=="!"
end
function evcond(cond)
  local i=1
  while i<=#cond and cond[i]==" " do i=i+1 end
  local var=""
  while i<=#cond and isalnum(cond[i]) do
    var=var..cond[i]
    i=i+1
  end
  while i<=#cond and cond[i]==" " do i=i+1 end
  local op=""
  while i<=#cond and isopchar(cond[i]) do
    op=op..cond[i]
    i=i+1
  end
  while i<=#cond and cond[i]==" " do i=i+1 end
  local val=sub(cond,i)
  while #val>0 and val[#val]==" " do
    val=sub(val,1,#val-1)
  end
  while #val>0 and val[1]==" " do
    val=sub(val,2)
  end
  if var=="" or op=="" then return false end
  local lv=getv(var)
  local rv=tonum(val)
  if not rv then rv=getv(val) end
  if op==">" then return lv>rv
  elseif op=="<" then return lv<rv
  elseif op==">=" then return lv>=rv
  elseif op=="<=" then return lv<=rv
  elseif op=="=" or op=="==" then return lv==rv
  elseif op=="!=" then return lv~=rv
  end
  return false
end

function evset(var,expr)
  if type(expr)=="number" then
    v[var]=expr
  else
    local s=expr
    if s[1]=="+" then
      v[var]=getv(var)+tonum(sub(s,2))
    elseif s[1]=="-" then
      v[var]=getv(var)-tonum(sub(s,2))
    else
      v[var]=tonum(s) or 0
    end
  end
end

function subvars(txt)
  local r=""
  local i=1
  while i<=#txt do
    if txt[i]=="$" and i<#txt and txt[i+1]=="{" then
      local j=i+2
      while j<=#txt and txt[j]~="}" do
        j=j+1
      end
      if j<=#txt then
        local vn=sub(txt,i+2,j-1)
        r=r..tostr(getv(vn))
        i=j+1
      else
        r=r..sub(txt,i)
        i=#txt+1
      end
    else
      r=r..txt[i]
      i=i+1
    end
  end
  return r
end

function wrp(txt)
  local lines={}
  local pos=1
  while pos<=#txt do
    local nl=nil
    for k=pos,#txt do
      if txt[k]=="\n" then
        nl=k
        break
      end
    end
    local line
    if nl then
      line=sub(txt,pos,nl-1)
      pos=nl+1
    else
      line=sub(txt,pos)
      pos=#txt+1
    end
    if #line==0 then
      add(lines,"")
    else
      while #line>ww do
        local br=ww
        local found=false
        for j=ww,1,-1 do
          if line[j]==" " then
            br=j
            found=true
            break
          end
        end
        if found then
          add(lines,sub(line,1,br-1))
          line=sub(line,br+1)
        else
          add(lines,sub(line,1,ww))
          line=sub(line,ww+1)
        end
        if line[1]==" " then
          line=sub(line,2)
        end
        if #line<=0 then break end
      end
      if #line>0 then
        add(lines,line)
      end
    end
  end
  return lines
end

function add_text(txt)
  if txt=="" then
    add(buf,"")
    return
  end
  local lines=wrp(subvars(txt))
  for i=1,#lines do
    add(buf,lines[i])
  end
end

function screen_full()
  return #buf - bl + 1 > maxl
end
function step()
  while true do
    local insts=sc()
    local pos=ci()
    if not insts or pos>#insts then
      if #es>1 then
        poplvl()
      else
        si=si+1
        if si<=#sl then
          load_scene(sl[si])
          return
        else
          st="done"
          return
        end
      end
    else
      local inst=insts[pos]
      local op=inst[1]

      if op==o_txt then
        add_text(inst[2])
        cur_lvl()[2]=pos+1
        if screen_full() then
          st="text"
          return
        end

      elseif op==o_ch then
        st="choice"
        ch_opts=inst[2]
        ch_sel=1
        cur_lvl()[2]=pos+1
        return

      elseif op==o_gt then
        local target=inst[2]
        local lvl,idx=findlb(target)
        if lvl then
          while #es>lvl do poplvl() end
          cur_lvl()[2]=idx
        else
          cur_lvl()[2]=pos+1
        end

      elseif op==o_lb then
        cur_lvl()[2]=pos+1

      elseif op==o_gs then
        load_scene(inst[2])
        return

      elseif op==o_fn then
        while #es>1 do poplvl() end
        si=si+1
        if si<=#sl then
          load_scene(sl[si])
          return
        else
          st="done"
          return
        end

      elseif op==o_pb then
        cur_lvl()[2]=pos+1
        st="pagebreak"
        return

      elseif op==o_st then
        evset(inst[2],inst[3])
        cur_lvl()[2]=pos+1

      elseif op==o_if then
        if evcond(inst[2]) then
          local then_insts=inst[3]
          cur_lvl()[2]=pos+1 
          if then_insts and #then_insts>0 then
            pushlvl(then_insts)
          end
        else
          cur_lvl()[2]=pos+1
        end

      elseif op==o_cr then
        v[inst[2]]=inst[3]
        cur_lvl()[2]=pos+1

      elseif op==o_gsb then
        local target=inst[2]
        local lvl,idx=findlb(target)
        if lvl then
          rs[#rs+1]={lvl=#es,pos=pos+1}
          while #es>lvl do poplvl() end
          cur_lvl()[2]=idx
        else
          cur_lvl()[2]=pos+1
        end

      elseif op==o_rt then
        if #rs>0 then
          local ret=rs[#rs]
          rs[#rs]=nil
          -- pop to return level
          while #es>ret.lvl do poplvl() end
          cur_lvl()[2]=ret.pos
        else
          cur_lvl()[2]=pos+1
        end

      elseif op==o_end then
        st="ending"
        return

      elseif op==o_lk then
        local link_text=inst[3] or inst[2]
        add_text("[link]"..link_text)
        cur_lvl()[2]=pos+1

      else
        cur_lvl()[2]=pos+1
      end
    end
  end
end

function restart_game()
  v={}
  rs={}
  si=1
  load_scene(sl[si])
end

1 Like