Log In  
Follow
Kaius
Picotron theme: PICO-8 Nostalgia
by
[ :: Read More :: ]

A custom theme for Picotron that tries to recreate the iconic colors of PICO-8's editors.

There are two ways to get this theme into your copy of Picotron. The first one is the more complicated manual process that I originally posted, and the second is just copy-pasting a command or two into the terminal:

Method 1: The manual way

  • If you don't already have /appdata/system/themes/, the easiest way to get it is to open up two filenav processes, with one at /system/ and the other at /appdata/system/, and drag the themes folder from the former into the latter. (I recommend doing it this way because if /appdata/system/themes/ exists, then the contents of /system/themes/ will be ignored, so simply creating an empty themes folder in /appdata/system/ will result in all of the system themes vanishing. Note that the same goes for wallpapers and screensavers, so while you have both of those filenavs open you might as well copy those over.)
  • open up your system settings by clicking on the Picotron logo on the top-left, then double-click the [custom] option in the themes chooser (or any of the themes; double-clicking any of them will work) to pull up the theme editor.
  • Manually copy the colors from the image above. (This is the tedious part. If you want to play with the colors a bit, go ahead.)
  • Click the menu button on the theme editor (the one in the top-left corner of the window that looks like three horizontal bars) and select Save File As.
  • Navigate to the /appdata/system/themes/ folder, type pico8.theme (or whatever you want to call it, but don't forget the .theme) into the prompt, and click Save.
  • Your theme will now show up in the list of available themes, though you may have to close and reopen the settings app if you left it open from earlier.

Method 2: The copy-paste-into-the-terminal-way


If you don't already have /appdata/system/themes/, the easiest way to get it is to copy this command into the terminal:

cp /system/themes/ /appdata/system/themes/

(I recommend doing this because if /appdata/system/themes/ exists, then the contents of /system/themes/ will be ignored, so simply creating an empty themes folder in /appdata/system/ will result in all of the system themes vanishing. Note that the same goes for wallpapers and screensavers, so don't forget to copy them over if you plan to add more of those.)

After that, just paste in this command to install the theme:

store("/appdata/system/themes/pico8.theme",unpod("b64:bHo0AE0BAAACAgAA8BJ7ZGVza3RvcF9wYXR0ZXJuPXB4dQBDIAwMBEcQBxAHADcGADAQNwAGABFXEgBBBxAHkAIACAgA4YAsZG9yX2ZyYW1lPTEzDQCTYnV0dG9uPTUsYgAxMT01GABxdGl0bGU9NhcAc19zaGFkb3clAEQwPTE3LgAyMD0xRgBxb3JkZXI9Mg4ASG1hbnQSAAIrAARFAAMSAPEHMT0xNCxpY29uMT02LHRvb2xiYXI9NxIAEzKcAAFIAAKtABEyGQA1MD03GAACoAAkMzE8ABAxNQAAJQA1Mz0xJQAD3QAVMiYAAVsAAwwAxV9zZWxlY3RlZD0xNRQAAEIACCQAZWl0ZW09Mg8ApGJhY2s9OCx3aW7hABIxDQACaQAhMTMOAAO0ABE5DQACqQAQNwwANGRvdzcAFTAQAAOjAAQQAAI8ABQ4DwCQdGl0bGU9MTV9"))


Enjoy!

P#144631 2024-03-27 06:15 ( Edited 2024-03-29 16:50)

[ :: Read More :: ]

On the latest episode of his advanced shmup tutorial, @Krystman showed off a function for splitting a string into a 2D array, and then said there could be more ways to make it more compact and token-efficient. Guess what I... didn't do?

Well, I didn't make it more token-efficient - my new version is close to double that of the original 28-token function at 55 tokens - but I DID make it more versatile; It can now handle:

  • 3,4,5 and onwards dimensional arrays (the original could only do 2-dimensional arrays)
  • Any character for use as a separator (the original could only use "|" and ",")
  • Optional tonum() conversion (the original had that forced on)

In fact, you could use this function exclusively instead of the built-in split() and you might not even notice. In order to get multi-dimensional arrays, the sep parameter should be given a string that is more than a single character long, with the most significant separator characters coming first. Oh, uh, after you paste the function into your code, that is. Speaking of which...

function splitf(str,sep,tnm)
 local cnt=type(sep)=="string" and #sep>1
 local arr=split(str,sep,not cnt and tnm!=false)
 if cnt then
  for k,v in pairs(arr) do
   arr[k]=splitf(v,sub(sep,2),tnm)
  end
 end
 return arr
end

if you want to get a good idea of how it behaves, paste the function definition into an empty cart, and then follow it up with this testing snippet:

function recursive_print(t,d)
 d=d or 0
 for k,v in pairs(t) do
  for i=1,9 do flip() end
  ?"\f2\*"..d.."|\fb"..k.."\f7: \fc"..tostr(v).."\f6 ("..type(v)..")"
  if type(v)=="table" then
   if d<8 then
    recursive_print(v,d+1)
   else
    ?"\f2\*"..1+d.."|\f8[ ** max depth reached ** ]"
   end
  end
 end
end

cls()
?"printing split string",13
recursive_print(splitf("1,2,3|1,2,3|1,2,3 1,2,3|1,2,3|1,2,3 1,2,3|1,2,3|1,2,3"," |,",true))

Of course, I'm sure I'm not the first person to come up with this function, and it's probably overkill in most cases. Still, if anyone wants to mess around with the function and make it more token-efficient, then I'll leave off by saying that the cnt local variable is the variable that determines if a recursive function call is necessary. (cnt for 'continue'. Previously I went with lst for 'last', but the continue flag was better at capturing the nuances of what happens when you give split() a non-string value for its separator.)

P#129884 2023-05-18 19:57 ( Edited 2023-05-18 19:58)

[ :: Read More :: ]

Here's a gif of me copying GFX data to code:

Here's a gif of me copying SFX data to code:

And here's a gif of me Copying music data to code:

So far, so good. Every bit of data copied also edits the clipboard, allowing these to be copy-pasted between instances of PICO-8.

But not so for map data! So here's a gif of me attempting to copy map data...

No edit to the clipboard was made. (Prior to each of these I had copied the -- TEST STRING TO COPY line to make it clearer.) Admittedly, you're not likely to ever need to copy map data between carts, but if you do (such as when working on a multicast game, or if you're transferring ideas from a prototype cart to the cart you plan to use for the full game) then it's gonna be real tricky to do so.

This issue was brought to my attention by the (as of now) latest episode from @Krystman on his advanced Shmup tutorial. Look around the 5:30 mark; he can easily copy the sprite sheet, but map data requires him to edit the P8 files directly, which works... but if you want to make the copied map data appear somewhere else rather than exactly where it was or only copy a small portion of the map, then that still presents a problem.

P#129139 2023-04-29 19:36

[ :: Read More :: ]

Yes! If you remember, back in January I had posted a code snippet that would let you play Minesweeper in the Picotron Playground. I later updated that snippet to have some comments describing the settings needed to recreate the three difficulty levels, but otherwise nothing changed. Well now, after months of (not that) hard (but still a little tricky) work, the third version of my Minesweeper game on Picotron is OUT! New features include:

  • Easy windowed mode: Running the program will instantly switch to the desktop. No more faffing about with run_program_inside_terminal! Who wants to type that out every single time?
  • A help screen: Just click the handy little blue question mark (?) and you can reference the rules anytime. Also shows up the first time the program is run.
  • In-game minefield customization: Nobody wants to edit the code every time just to change the difficulty. Nobody.
  • Highscores: Though they can't be saved between sessions, they CAN be saved if you close the program and re-run it without refreshing the browser tab, and it should work on the executable version of Picotron when that's released.
  • A screenshot button: Though it sucks and I wouldn't use it, since the BBS won't see colors #16-31 as anything other than black, so you can't post them here. :/

HOW TO GET STARTED

First, copy the following hidden block of code: (The -- end of program line should be #702)

-- Minesweeper (v3)
-- By: Kai
-- Other things I might want to do:
-- o Unique font for 7-seg displays? (Dunno how to use custom fonts on Picotron yet...)
-- o Release to SPLORE. (When Picotron is released, obviously.)
-- o Make the screenshot function return a PNG file instead? (If it's possible...)
-- o Maybe online time submissions? (Again, if it's possible...)
--
-- GFX decoder
function decode(str) -- necessary so the forum doesn't screw up the code
    return userdata(chr(91,103,102,120,93)..str..chr(91,47,103,102,120,93))
end
--
-- GFX: faces
smile=decode"101077777777777777767666666666666665766660000006666576660aaaaaa066657660aaaaaaaa0665760aa00aa00aa065760aa00aa00aa065760aa00aa00aa065760aaaaaaaaaa065760aaaaaaaaaa065760aa0aaaa0aa0657660aa0000aa066576660aaaaaa06665766660000006666576666666666666656555555555555555"
curious=decode"101077777777777777767666666666666665766660000006666576660aaaaaa066657660aaaaaaaa0665760aa00aa00aa065760aa00aa00aa065760aa00aa00aa065760aaaaaaaaaa065760aaaaaaaaaa065760aaaa00aaaa0657660aaa00aaa066576660aaaaaa06665766660000006666576666666666666656555555555555555"
frown=decode"101077777777777777767666666666666665766660000006666576660aaaaaa066657660aaaaaaaa0665760aa0aaaa0aa065760aaa0aa0aaa065760aa0aaaa0aa065760aaaaaaaaaa065760aaa0000aaa065760aa0aaaa0aa0657660aaaaaaaa066576660aaaaaa06665766660000006666576666666666666656555555555555555"
cool=decode"101077777777777777767666666666666665766660000006666576660aaaaaa066657660aaaaaaaa0665760a000aa000a0657600000000000065760a000aa000a065760aaaaaaaaaa065760aa0aaaa0aa065760aaa0000aaa0657660aaaaaaaa066576660aaaaaa06665766660000006666576666666666666656555555555555555"
-- GFX: tiles
boom=decode"08088888888m8686868m8855588m8656568m8855588m8686868m8888888mmmmmmmmm"
block=decode"08087777776m7666665m7666665m7666665m7666665m7666665m6555555mmmmmmmmm"
empty=decode"0808kkkkkkkmk444444mk444444mk444444mk444444mk444444mk444444mmmmmmmmm"
flag=decode"08087777776m7668665m7688665m7888665m766i665m76iii65m6555555mmmmmmmmm"
mine=decode"0808kkkkkkkmk646464mk455544mk656564mk455544mk646464mk444444mmmmmmmmm"
falseflag=decode"08087777778m7662685m7622865m7228665m768i665m78iii65m8555555mmmmmmmmm"
-- GFX: numbers
numbercols={12,27,24,16,2,17,13,0} numbers={
decode"08080000000000770000000700000007000000070000007770000000000000000000",
decode"08080000000000777000000070000077700000700000007770000000000000000000",
decode"08080000000000777000000070000007700000007000007770000000000000000000",
decode"08080000000000707000007070000077700000007000000070000000000000000000",
decode"08080000000000777000007000000077700000007000007770000000000000000000",
decode"08080000000000700000007000000077700000707000007770000000000000000000",
decode"08080000000000777000000070000000700000007000000070000000000000000000",
decode"08080000000000777000007070000077700000707000007770000000000000000000"}
-- GFX: buttons
wrench=decode"0808777777767666m6657666m665766mmmm576mmm6657m6m66657mm6666565555555"
help=decode"08087777777676hhhh657hh6hhh57666hhh5766hh66576666665766hh66565555555"
left=decode"0808777777767666mm65766mmm6576mmmm6576mmmm65766mmm657666mm6565555555"
right=decode"08087777777676mm666576mmm66576mmmm6576mmmm6576mmm66576mm666565555555"
pause=decode"08087777777676m66m6576m66m6576m66m6576m66m6576m66m6576m66m6565555555"
fishhook=decode"08087777777676mmmm657m6666m5766666m576mm66m57m6mmm6576mm666565555555"
unusable=decode"0808777777767o6666o576o66o65766oo665766oo66576o66o657o6666o565555555"
cam=decode"080877777776766mm6657mmmmmm57mm66mm57mm66mm57mmmmmm57666666565555555"
--
-- adjustables
-- beginner: 9x9/10
-- intermediate: 16x16/40
-- expert: 30x16/99
-- WARNING!!! Setting the size of the minefield too high will cause Picotron to start
-- flashing the screen, rendering the game virtually unplayable and pose a risk to those
-- vulnerable to said flashing. User discretion is advised.
-- DO NOT ADJUST as of v3. PLEASE keep these at 9x9/10 and
-- use the in-game editor to change these.
width=9 -- min: 9 | max: 59
height=9 -- min: 1 | max: 28
mines=10 -- idealy ~10-20% of total tiles
--
-- other setup
lmb=0
dead=false
won=false
start=true
paused=false
remainingmines=mines
timer=0
set_window(width*8+1,height*8+20)
cheatinput={}
-- highscores setup
highscores_txt=fetch"/best_minesweeper_times.txt"
if not highscores_txt then
    highscores_txt="999,999,999\n999,999,999\n999,999,999"
    store("/best_minesweeper_times.txt",highscores_txt)
    first_time=true
end
highscores_arr=split(highscores_txt,"\n")
for i=1,3 do
    this_diff=split(highscores_arr[i],",")
    for j=1,3 do
        this_diff[j]=tonumber(this_diff[j])
    end
    highscores_arr[i]=this_diff
end
difficulty=1
--
-- data format:
-- bit 0: mine
-- bit 1: uncovered
-- bit 2: flagged
-- bit 3: UNUSED
-- bit 4: UNUSED
-- bit 5: UNUSED
-- bit 6: UNUSED
-- bit 7: UNUSED
--
minefield=userdata("u8",width,height)
--
-- debug: randomized test board
--for x=0,width-1 do
    --for y=0,height-1 do
        --set(minefield,x,y,flr(rnd(2)))
    --end
--end
--dead=true
-- /debug
--
function place_mines(cx,cy)
    local goodcandidates={}
    local nearcandidates={}
    for x=0,width-1 do
        for y=0,height-1 do
            if abs(x-cx)>1 or abs(y-cy)>1 then
                add(goodcandidates,{x,y})
            elseif x~=cx or y~=cy then
                add(nearcandidates,{x,y})
            end
        end
    end
    for i=1,mines do
        --local position=candidates[flr(rnd(#candidates))+1]
        local position
        if #goodcandidates>0 then
            position=deli(goodcandidates,flr(rnd(#goodcandidates))+1)
        else
            position=deli(nearcandidates,flr(rnd(#nearcandidates))+1)
        end
        set(minefield,position[1],position[2],1)
        --del(candidates,position)
    end
end
--
function bit(n,b,c)
    if type(c)=="boolean" then
        return c and n|2^b or n&-1-2^b
    else
        return n&2^b>0
    end
end
--
function uncover(tx,ty)
    if (bit(get(minefield,tx,ty),1)) return -- already uncovered
    if (bit(get(minefield,tx,ty),2)) return -- flagged
    if (tx<0 or tx>=width or ty<0 or ty>=height) return -- out of bounds
    set(minefield,tx,ty,get(minefield,tx,ty)+2)
    local nearbymine
    for xdelta=-1,1 do
        for ydelta=-1,1 do
            if (bit(get(minefield,tx+xdelta,ty+ydelta),0)) nearbymine=true
        end
    end
    if not nearbymine then
        for xdelta=-1,1 do
            for ydelta=-1,1 do
                local x=tx+xdelta
                local y=ty+ydelta
                if (x==mid(0,x,width-1) and y==mid(0,y,height-1) and not bit(get(minefield,x,y),1) and not bit(get(minefield,x,y),2)) uncover(x,y)
            end
        end
    elseif bit(get(minefield,tx,ty),0) then
        dead=true
    end
end
--
function checkwon()
    if (dead) return
    local coveredtiles=0
    for x=0,width-1 do
        for y=0,height-1 do
            if (not bit(get(minefield,x,y),1)) coveredtiles+=1
        end
    end
    if coveredtiles==mines then
        won=true
        remainingmines=0
        local best_times=highscores_arr[difficulty]
        local score=flr(timer)
        rank=0
        if cheatinput and difficulty>0 and score< best_times[3] then
            best_times[3]=score rank=3
            if (best_times[3]< best_times[2]) best_times[3]=best_times[2] best_times[2]=score rank=2
            if (best_times[2]< best_times[1]) best_times[2]=best_times[1] best_times[1]=score rank=1
            --if (score< world_recoreds[difficulty]) rank="*" scoresub() -- world record!
            highscores_arr[difficulty]=best_times -- probably unnecessary
            highscores_txt=highscores_arr[1][1]..","..highscores_arr[1][2]..","..highscores_arr[1][3].."\n"..highscores_arr[2][1]..","..highscores_arr[2][2]..","..highscores_arr[2][3].."\n"..highscores_arr[3][1]..","..highscores_arr[3][2]..","..highscores_arr[3][3]
            rm"/best_minesweeper_times.txt"
            store("/best_minesweeper_times.txt",highscores_txt)
        end
        if (not cheatinput) rank="x"
        if (difficulty==0) rank="?"
    end
end
--
function draw_game()
    if (not (dead or won or start or paused)) timer=min(timer+1/60,999)
    local tx=(mx-1)\8
    local ty=(my-20)\8
    if tx==mid(0,tx,width-1) and ty==mid(0,ty,height-1) and not dead and not won and not paused then
        -- on grid
        if bit(mbp,0) and not bit(get(minefield,tx,ty),1) and not bit(get(minefield,tx,ty),2) then
            if (start) place_mines(tx,ty) start=false
            --set(minefield,tx,ty,bit(get(minefield,tx,ty),1,true))
            uncover(tx,ty)
            checkwon()
        elseif bit(mbp,2) and not bit(get(minefield,tx,ty),1) and not start then
            set(minefield,tx,ty,get(minefield,tx,ty)^^4)
            remainingmines+=bit(get(minefield,tx,ty),2) and -1 or 1
        elseif bit(mbp,1) and bit(get(minefield,tx,ty),1) then
            local closemines=0
            local closeflags=0
            for xdelta=-1,1 do
                for ydelta=-1,1 do
                    if (bit(get(minefield,tx+xdelta,ty+ydelta),0)) closemines+=1
                    if (bit(get(minefield,tx+xdelta,ty+ydelta),2)) closeflags+=1
                end
            end
            if closemines==closeflags then
                for xdelta=-1,1 do
                    for ydelta=-1,1 do
                        uncover(tx+xdelta,ty+ydelta)
                    end
                end
                checkwon()
            end
        end
    elseif mx==mid(width*4-8,mx,width*4+7) and my==mid(2,my,17) and bit(mbp,0) then -- clicked on face
        -- reset puzzle
        dead=false
        won=false
        start=true
        paused=false
        remainingmines=mines
        timer=0
        for x=0,width-1 do
            for y=0,height-1 do
                set(minefield,x,y,0)
            end
        end
        rank=nil
    elseif mx==mid(width*4+10,mx,width*4+17) and my==mid(6,my,13) and bit(mbp,0) then -- clicked on wrench
        if start then
            draw_function=draw_menu
            if (not (dead or won or start)) paused=true
            cheatinput={}
        elseif dead or won then
            -- screenshot function
            local screenshot=get_draw_target()
            local clipboard_txt=chr(91,103,102,120,93)
            local w,h=tww,twh
            local function to_p8scii_num(n)
                if n<10 then
                    return tostr(flr(n))
                else
                    return chr(flr(n+87))
                end
            end
            clipboard_txt..=to_p8scii_num(flr(w/16))..to_p8scii_num(w%16)..to_p8scii_num(flr(h/16))..to_p8scii_num(h%16)
            for i=0,w*h-1 do
                clipboard_txt..=to_p8scii_num(get(screenshot,i%w,i\w))
            end
            clipboard_txt..=chr(91,47,103,102,120,93)
            set_clipboard_text(clipboard_txt)
            cls() return
        else
            paused=not paused
        end
    elseif mx==mid(width*4-18,mx,width*4-11) and my==mid(6,my,13) and bit(mbp,0) then -- clicked on help
        draw_function=draw_help
        if (not (dead or won or start)) paused=true
    end
    --
    -- draw game
    cls()
    rectfill(0,0,width*8,19,22)
    rectfill(0,19,width*8,height*8+19,32)
    palt(0,false)
    local emote=dead and frown or won and cool or (bit(mb,0) or bit(mb,1)) and tx==mid(0,tx,width-1) and ty==mid(0,ty,height-1) and curious or smile
    spr(emote,width*4-8,2)
    spr(start and wrench or (dead or won) and cam or paused and right or pause,width*4+10,6)
    spr(help,width*4-18,6)
    palt(0,true)
    if paused then
        print("** PAUSED **",width*4-30,height*4+15,7)
    else
        for x=0,width-1 do
            for y=-0,height-1 do
                local tile
                local tdata=get(minefield,x,y)
                local showneighbors
                if bit(tdata,1) then -- uncovered
                    if bit(tdata,0) then -- mine
                        tile=boom
                    else -- no mine
                        tile=empty
                        showneighbors=true
                    end
                else -- covered
                    if bit(tdata,2) or won then -- flag
                        if bit(tdata,0) or not dead then -- correct or still playing
                            tile=flag
                        else -- game over corrections
                            tile=falseflag
                        end
                    else -- no flag
                        if bit(tdata,0) and dead then -- game over clairvoyance
                            tile=mine
                        else -- nothing special
                            tile=block
                        end
                    end
                end
                spr(tile,x*8+1,y*8+20)
                if showneighbors then
                    local closemines=0
                    for xdelta=-1,1 do
                        for ydelta=-1,1 do
                            if (bit(get(minefield,x+xdelta,y+ydelta),0)) closemines+=1
                        end
                    end
                    pal(7,numbercols[closemines])
                    spr(numbers[closemines],x*8+1,y*8+20)
                    pal(7,7)
                end
            end
        end
    end
    --rect(0,0,width*8,19,1)
    rect(0,19,width*8,height*8+19,17)
    rectfill(1,5,16,13,2)
    local str=mid(-99,flr(remainingmines),999)
    if str<-9 then
        -- do nothing
    elseif str<0 then
        str="-0"..-str
    elseif str<10 then
        str="00"..str
    elseif str<100 then
        str="0"..str
    else
        -- do nothing
    end
    print(({[0]="non","1St","2nd","3rd",["*"]="tOP",["x"]="CHt",["!"]=str,["?"]="CUS"})[rank or "!"],2,6,8)
    --print("888\f8\-1"..({[0]="non","1St","2nd","3rd",["*"]="tOP",["x"]="CHt",["!"]=str,["?"]="CUS"})[rank or "!"],2,6,24)
    rectfill(width*8-16,5,width*8-1,13,2)
    local str=mid(-99,flr(timer),999)
    if str<-9 then
        -- do nothing
    elseif str<0 then
        str="-0"..-str
    elseif str<10 then
        str="00"..str
    elseif str<100 then
        str="0"..str
    else
        -- do nothing
    end
    print(str,width*8-15,6,8)
    --print("888\f8\-1"..str,width*8-15,6,24)
    if cheatinput then
        for i=97,122 do
            if (get_key_pressed(chr(i))) add(cheatinput,i)
        end
        while #cheatinput>5 do
            deli(cheatinput,1)
        end
        if (#cheatinput>=5 and cheatinput[1]..cheatinput[2]..cheatinput[3]..cheatinput[4]..cheatinput[5]=="120121122122121" and get_key_state"shift") cheatinput=nil
    else -- cheating!!!
        pset(0,0,(tx<0 or tx>=width or ty<0 or ty>=height) and 5 or bit(get(minefield,tx,ty),0) and 32 or 7)
        --pset(width*8,0)
    end
    -- debug: display mouse statistics
        --local ww,wh=window_size()
        --cursor(width*8+2,1) color(27)
        --?"mx: "..mx
        --?"my: "..my
        --?"mb: "..mb
        --?"mbp: "..mbp
        --?"tx: "..tx
        --?"ty: "..ty
        --?"ww: "..ww
        --?"wh: "..wh
        --?"b: "..highscores_arr[1]
        --?"i: "..highscores_arr[2]
        --?"e: "..highscores_arr[3]
    -- /debug
    --
    tww=width*8+1
    twh=height*8+20
end
--
function draw_menu()
    if mbp%2==1 then
        if mx==mid(150,mx,157) and my==mid(70,my,77) then
            draw_function=draw_game
            remainingmines=mines
            minefield=userdata("u8",width,height)
        elseif mx==mid(0,mx,6) and my==mid(21,my,63) then
            for i=2,5 do
                -- check if we are close to the appropriate difficulty selector
                if abs(mx-3)^2+abs(my-i*12)^2<9 then
                    difficulty=i-1
                    if difficulty==4 then
                        difficulty=0
                    else
                        width= ({09,16,30})[difficulty]
                        height=({09,16,16})[difficulty]
                        mines= ({10,40,99})[difficulty]
                    end
                    break
                end
            end
        elseif difficulty==0 and mx==mid(131,mx,157) and my==mid(13,my,44) then
            local change=get_key_state"ctrl" and get_key_state"shift" and 100 or get_key_state"ctrl" and 10 or get_key_state"shift" and 5 or 1
            if (mx==mid(131,mx,138) and my==mid(13,my,20)) width-=change
            if (mx==mid(150,mx,157) and my==mid(13,my,20)) width+=change
            if (mx==mid(131,mx,138) and my==mid(25,my,32)) height-=change
            if (mx==mid(150,mx,157) and my==mid(25,my,32)) height+=change
            if (mx==mid(131,mx,138) and my==mid(37,my,44)) mines-=change
            if (mx==mid(150,mx,157) and my==mid(37,my,44)) mines+=change
            width=mid(9,width,30)
            height=mid(9,height,16)
            mines=mid(10,mines,min(99,width*height-1))
        end
    end
    --
    cls(22)
    ?"Minefield Customization Menu",1,1,23
    spr(fishhook,150,70)
    ?"Beginner\nIntermediate\nExpert\nCustom",7,21,6
    for i=2,5 do
        local y=i*12
        if (i-1==difficulty or i==5 and difficulty==0) circfill(3,y,2,27)
        circ(3,y,2,6)
    end
    if difficulty==0 then
        ?" width:\nheight:\n mines:",96,13,6
        for y=12,36,12 do
            rectfill(130,y,158,y+9,32)
            ?y==12 and (width<10 and "0"..width or width) or y==24 and (height<10 and "0"..height or height) or mines,140,y+1,6
        end
        spr(width>9 and left or unusable,131,13)
        spr(width<30 and right or unusable,150,13)
        spr(height>9 and left or unusable,131,25)
        spr(height<16 and right or unusable,150,25)
        spr(mines>10 and left or unusable,131,37)
        spr(mines< min(99,width*height-1) and right or unusable,150,37)
    else
        local best_times=highscores_arr[difficulty]
        ?"1st: "..best_times[1].."\n2nd: "..best_times[2].."\n3rd: "..best_times[3],111,13,6
    end
    --
    tww=160
    twh=80
end
--
function draw_help()
    page=page or 1
    local pages={{[[
Welcome to Picotron Minesweeper!

Click the   to continue reading.
Click the   to go back a page.
Click the   to exit this screen.
]],
spr,right,49,25,
spr,left,49,37,
spr,fishhook,49,49},
{[[
Note that the game is paused in
this menu, so don't worry about
looking back here in the middle
of a game when you need help.
Click the   to get back here.
]],
spr,help,49,49
},{[[
Click a \0121tile\012s (  ) to uncover it.
A number will be revealed.

This number shows how many
\0121mines\012s (  ) surround the tile.
]],
rectfill,71,0,79,8,22,
spr,block,72,1,
rectfill,36,48,44,56,22,
spr,boom,37,49,
function()
    rectfill(1,24,73,32,22)
    for i=0,8 do
        local x=i*8+2
        spr(empty,x,25)
        pal(7,numbercols[i])
        spr(numbers[i],x,25)
        pal(7,7)
    end
end},
{[[
\0121Uncovering a mine will end the
game.\012s The goal is to uncover
all non-mine tiles as \0121quickly
as possible.\012s
]]},{[[
Right-click a tile to \0121flag\012s (  )
it as having a mine underneath.

Use this to keep track of what
you think is and isn't a mine.
]],
rectfill,141,0,149,8,22,
spr,flag,142,1},
{[[
The number in the upper-left
displays \0121an approximation of
how many mines are remaining.\012s
The number in the upper-right
displays your current \0121time.\012s
]]},{[[
If a tile with \0121zero\012s neighboring
mines is uncovered, \0121all\012s nearby
tiles will automatically be
uncovered. Your first click is
\0121guaranteed\012s to land on such a
tile, if at all possible.
]]},{[[
To restart the game, click the
\0121face\012s in the top-middle of the
screen. Its face also \0121corres-
ponds to the current game state.\012s
]],
pal,0,0,
spr,smile,35,50,
spr,curious,60,50,
spr,frown,85,50,
spr,cool,110,50,
palt,0,true},
{[[
If you middle-click a revealed
tile, \0121all surrounding non-
flagged tiles\012s will be opened \0121IF\012s
the \0121number of the clicked tile
is equal to the number of
surrounding flags.\012s
]]},
{[[
To the left of the face is
that   that you can click to
get back to this help screen,
but you probably figured that
out already.
]],
spr,help,24,13},
{[[
To the \0121right\012s of the face,
however, is a button that has
multiple functions \0121depending
on the game's state.\012s
]],
spr,wrench,100,37,
spr,pause,110,37,
spr,right,120,37,
spr,cam,130,37},
{[[
When the game ends (one way
or another), the \0121camera button\012s
(  ) shows up to let you get a
\0121screenshot\012s as a \0121Picotron GFX 
string copied to your clipboard.\012s
(experimental!)
]],
spr,cam,7,25},
{[[
In the middle of a game,
the \0121pause\012s (  ) and \0121unpause\012s (  )
buttons can be used if you
need to take a break
for whatever reason.
]],
spr,pause,57,13,
spr,right,142,13},
{[[
The most interesting option
though is at the beginning of
a round with the \0121Minefield
Customization Menu.\012s (  )
]],
spr,wrench,107,37},
{[[
In this menu, you can switch to
one of \0121three difficulty levels\012s
and see your \0121top three scores\012s
for each of them.
]]},{[[
You can also create a minefield
with \0121custom parameters,\012s such as
\0121width, height and mines\012s using
the   and   buttons.
]],
spr,left,19,37,
spr,right,49,37},
{[[
After winning the game, the
\0121estimated mine count\012s will
switch to showing how well you
did. \0121'non'\012s means no best time,
while \0121'1St'\012s, \0121'2nd'\012s and \0121'3rd'\012s
mean the appropriate place.
]]},
{[[

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
\0121That's about all there is to
Picotron Minesweeper. Have fun!\012s
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
]]} -- ...Aside from the hidden *CHEAT CODE*, that is...
--,{[[
--\0121Push\012s button
--Recieve \0121bacon\012s
--]]}
}
    --
    if mbp%2==1 and my==mid(70,my,77) then
        if mx==mid(150,mx,157) and page<#pages then
            page+=1
            first_time=nil
        elseif mx==mid(140,mx,147) and page>1 then
            page-=1
        elseif mx==mid(120,mx,127) and not first_time then
            --page=nil -- reset current page number (I decided against this later)
            draw_function=draw_game
            --if (not page) return -- avoid a crash if we reset the page number
        end
    end
    --
    cls(17)
    local to_draw=pages[page]
    local i=0
    local text=to_draw[1]
    while i<#text do
        i+=1
        if sub(text,i,i)=="\\" then
            text=sub(text,1,i-1)..chr(tonumber(sub(text,i+1,i+3)))..sub(text,i+4,#text)
        end
    end
    ?text,1,1,28
    --palt(22,true)
    --for i=2,#to_draw,3 do
        --spr(to_draw[i],to_draw[i+1],to_draw[i+2])
    --end
    --if page==3 then
        --pal(22,22) rectfill(1,24,73,32,22) palt(22,true)
        --for i=0,8 do
            --local x=i*8+2
            --spr(empty,x,25)
            --pal(7,numbercols[i])
            --spr(numbers[i],x,25)
            --pal(7,7)
        --end
    --end
    --pal(22,22)
    i=2
    while i<=#to_draw do
        local func=to_draw[i]
        i+=1
        local args={}
        while type(to_draw[i])~="function" and to_draw[i]~=nil do
            add(args,to_draw[i])
            i+=1
        end
        func(table.unpack(args))
     end
    ?"Page: "..page.."/"..#pages,2,71,12
    spr(first_time and unusable or fishhook,120,70)
    spr(page==1 and unusable or left,140,70)
    spr(page==#pages and unusable or right,150,70)
    --
    tww=160
    twh=80
end
--
draw_function=first_time and draw_help or draw_game
--
function _draw()
    -- mouse handling
    mx,my,mb=get_mouse()
    mbp=mb&~lmb -- mb pressed
    lmb=mb -- last mb
    --
    draw_function() -- varies
    --
    -- housekeeping stuff (their necessity is mostly Picotron's fault)
    rnd() -- sufficiently jumbles up the RNG seeds
    -- (necessary due to Picotron not randomizing the seed on startup)
    -- next up: a bunch of stuff to prevent you from resizing the window manually
    local ww,wh=window_size()
    if (tww~=ww or twh~=wh) set_window(tww,twh)
    -- more complicated than it needs to be because set_window() has WEEEEEIIIIIRRRRRD
    -- effects on how the drawing works. for example, sometimes it will prevent all
    -- future drawing actions from taking place. the solution would be to set the window
    -- last. HOWEVER, sometimes it will allow future drawing, but CLEAR THE SCREEN TOO.
    -- in that case, you would want to set the window first thing! long story short,
    -- to make thing easier we only fix the window size if we really, really have to.
end
-- end of program


Then paste it into a blank codebase in Picotron Playground, hit Ctrl+R and... THAT'S IT.

By default, you will be booted into the help screen, but if you don't care for that, just click the 'next page' button, and then the back button will show up so you can skip straight to playing the game.

Technical notes

(Warning: Long wall of text follows)


Most of the streamlining goes to set_window(w,h). With this function, the program will automatically make it's own window, set it's size, and flip to the desktop view, creating the desktop if it hasen't been booted up this session. If it's called multiple times, it will instead adjust the window it had already made earlier, though with some inconsistency as to how it treats anything already on the screen, and how it treats newly drawn stuff. (In my experience, one or the other will continue working, but never both, but which one works and which on doesen't seem to switch randomly whenever you do something like _draw=draw_game and I have no idea why. Earlier in development I edited the _draw function like this, but I decided against that when I needed some stuff like the mouse variables to always be updated, which inadvertently fixed the inconsistency issue, as it I just tested it now and it seem to be stuck on the 'lock future drawing, don't clear screen' mode, whereas before when I switched the _draw function around it seemed to switch modes. Still, I don't want to risk anything going wrong, so I still do the check to make sure it's actually necessary to adjust the window, just in case.) Credit to @pancelor for their function-lister, as otherwise this would not have been possible.

I also changed the font to be smaller, because that old font looked waayyy too big, and didn't go well at ALL with the shading on the holes, particularly with some colors (for example, the purple of the 4 combined with the shading to make it look like an 'A').

Did you know that the original Minesweeper had a cheat code? This version has that cheat code implemented too, so if you know the code you can solve puzzles that would of otherwise been impossible without guessing. (Though, you won't be able to save your score if do so.) Want to know the code? Well... for that, you're gonna have to look in the game's code. You gotta EARN it, you know?

Pasting the program into PICO-8 shows that this program is consists of 2944 tokens, 24445 chars, and 7642 compressed bytes (48% of .p8.png capacity). Of course, the program won't work in PICO-8 - it crashes just from attempting to define the sprites - but it's still a good metric for how much work I put in. (Answer: Actually not that much, at least compared to some other people.)

If you have too many empty tiles onscreen at any one time, this happens:

Actually, it's even worse, as it constantly swaps between this and the normal desktop at sixty frames a second. So, uh, I suppose I should give out an epilepsy warning, I guess?

Also, posting this on here was... harder than you would think. For starters, there's that gosh-darned decode function at the top of the codebase. Why is that? Well...

jelpi=userdata"
	
[8x8]

That looks horrible! And yes, that is what happens when you try to put a {gfx}..{/gfx} tag (imagine those as being square brackets and not curly ones) in a post, even in a section marked as code. The decode function's job is to synthesize one of those tags without actually having one of them in the code. So to make that Jelpi sprite:

function decode(str)
    return userdata(chr(91,103,102,120,93)..str..chr(91,47,103,102,120,93))
end
jelpi=decode"0808000000000f000f000ffffff00f1fff100effffe0002220000088800000f0f000"

You can see that the chr function is helping out greatly here by letting us encode the gfx portion as a series of numbers, and then decoding them to feed them to userdata.

The other issue I had when posting this code here was statements like blah< foo. If you remove the space between < and foo, it will remove ALL code between that point and whenever it finally sees a >. This doesn't seem to happen when a number or a space is immediatly following the < however, so fortunately this was relatively easy to fix.

That's about all I had. Like others, I probably won't continue to work on anything major in Picotron until a new version comes out (particularly the 0.1 release), but I still enjoyed working on it nonetheless. Special thanks to @Liquidream, @dw817 and @merwok for the kind words spoken about this project that helped me motivate myself to continue working on it.

P#128198 2023-04-07 21:33

[ :: Read More :: ]

One nice thing about objects is that objects set with the = operator or as arguments are all 'linked' meaning, among other things, you can provide them as arguments for a function call and that function will be able to edit the local copy of that object and have it affect the original one too. But sometimes, you don't want that. Sometimes you want to copy an object, make changes to it, and then discard the new copy without affecting the old one. That's why I've created a little helper function for this circumstance. Actually, I've created several. Here's the fully-featured 'default' function:

function cpy_obj(obj,recursion)
    if type(obj)=="table" then
        local return_value={}
        for k,v in pairs(obj) do
            if recursion then
                v=cpy_obj(v,true)
            end
            return_value[k]=v
        end
        return return_value
    else
        return obj
    end
end

Usage: copy= cpy_obj(𝘰𝘣𝘫𝘦𝘤𝘵) or function_call( cpy_obj(𝘰𝘣𝘫𝘦𝘤𝘵) )

return_value is the new object being created. Note that since we are creating it a piece at a time, it has no affiliation with the previous object. If you give it something that is not an object, it will just give you the input variable back. recursion is a boolean that asks if we want to make nested objects be decoupled as well. You usually want this if you have nested objects, but keep in mind that this comes with a performance cost, and although it's rather small, you might not want that if you're pushing up against the limits of PICO-8.

Variants

Every project is different, and with that comes different requirements. One person will be working on a game jam that has no risk of reaching the token limit, and another will be working on a 3D engine that needs to be as compact as possible. As such, I have a few different variants that may cater to your project's particular demands...

Forced recursion

function cpy_obj(obj)
    if type(obj)=="table" then
        local return_value={}
        for k,v in pairs(obj) do
            v=cpy_obj(v)
            return_value[k]=v
        end
        return return_value
    else
        return obj
    end
end


This one shaves off five tokens (plus one every time you need recursion!), but comes at the downside of a forced performance cost. Still, this one is probably closer to being the more useful one than the default one due to the token savings.

No recursion

function cpy_obj(obj)
    if type(obj)=="table" then
        local return_value={}
        for k,v in pairs(obj) do
            return_value[k]=v
        end
        return return_value
    else
        return obj
    end
end


This one saves five more tokens than the previous one, but lacks recursion entirely. Of course, you won't always (and usually don't) need recursion, but if you do then this function won't cut it for you.

No non-object return values

function cpy_obj(obj)
    local return_value={}
    for k,v in pairs(obj) do
        return_value[k]=v
    end
    return return_value
end


This one save TEN more tokens than even the last one, but non-object inputs will result in a crash as the API function pairs() won't know what to do.

Ultra-cheap snippet

𝘯𝘦𝘸_𝘰𝘣𝘫={}
for k,v in pairs(𝘰𝘭𝘥_𝘰𝘣𝘫) do
    𝘯𝘦𝘸_𝘰𝘣𝘫[k]=v
end


This one is cheaper than all of the others at just thirteen tokens, but must be copy-pasted everywhere you intend to use it meaning it only saves tokens if you only use it in one or two spots (which very well may be the case). Whatever you do, be sure to replace 𝘰𝘭𝘥_𝘰𝘣𝘫 and 𝘯𝘦𝘸_𝘰𝘣𝘫 with your object names.

Comparisons

Lastly, here's a list of the token costs of each function, assuming all arguments are one variable:

Default                     : 42 to define  , 3 or 4 per call
Forced recursion            : 37 to define  ,      3 per call
No recursion:               : 32 to define  ,      3 per call
No non-object return values : 22 to define  ,      3 per call
Ultra-cheap snippet         : no definition ,     13 per call
P#122225 2022-12-11 07:27