Max Glenister

Series

Posts that belong together, grouped and in order.

8 series · 56 posts

How this blog is built

4 posts · January 2026 to October 2026

  1. Moving to GitHub Actions (and adding .txt posts)

    This year I moved the blog from GitHub Pages’ built-in Jekyll to a GitHub Actions workflow, then added .txt URL support for posts. Inspired by Terence Eden’s blog post about text-only websites.

    • 4 min read • 4 interactions
  2. Plugins instead of patches

    I’ve been running Known as my social posting hub for a while now. It’s a solid IndieWeb-friendly CMS that handles status updates, photos, and syndication to various silos. Recently I upgraded my instance for the first time in about four years, which meant dealing with all the little tweaks I’d made to core templates over that time. Previously I’d just edited core files directly, which meant upgrades would wipe my changes. After four years I had no idea what I’d even modified (I’ll go into the archaeology of that in another post). This time I decided to do it properly, and ended up writing four plugins.

    • 3 min read
  3. Reducing commit log noise caused by a GitHub Action

    I have a “now playing” widget on my /now page that shows the last track scrobbled to Last.fm. It’s powered by a GitHub Actions workflow that polls the Last.fm API every hour and commits the result to the repo. It worked fine, but quietly polluted my commit history.

    • 4 min read • 1 interaction
  4. Chasing build speed on Jekyll 4

    I’ve been putting off upgrading from Jekyll 3.10. Partly late-adopter habit, partly because my setup worked fine, and a version bump seemed simple enough. I should know better by now. It mostly was.

    • 4 min read • 1 interaction

Webmentions

4 posts · April 2022 to June 2026

  1. Adding support for Webmentions

    I’ve added Webmention support to the posts on this blog. Webmentions are a method for websites to know that they’ve been linked to (or mentioned) from elsewhere on the web.

    • 3 min read • 37 interactions
  2. Baking webmentions into the build

    Back in 2022 I added webmention support to this blog. The implementation worked but it was entirely client-side, so the page would load, JS would fire, fetch from webmention.io, and render the results into the DOM. With JS disabled or on a slow connection, the discussion section was just empty. It worked well enough that I didn’t touch it for three years, but the rate-limit situation had been silently catching visitors out and eventually I started pulling at the thread.

    • 5 min read • 2 interactions
  3. Sending webmentions on deploy

    I’ve spent a fair bit of time on the receiving end of webmentions, from adding support for them in the first place through to fetching them at build time and rendering them server-side so the discussion section doesn’t flicker in over JS. What I’d never actually done was send the other half. Every link I’ve ever made out to another IndieWeb site has just sat there, silently not telling anyone I’d linked to them.

    • 3 min read • 2 interactions
  4. Enriching webmentions from third-party platforms

    I keep finding new gaps in my webmention implementation. Mentions coming in from Bluesky, Lemmy and Lobsters were showing up with no author at all, just a blank avatar and an empty name. Reddit and Hacker News bookmarks fared a bit better, with a username attached, but got flattened into a generic “bookmarked” icon with no indication of where the post had actually been shared. Lobsters mentions also carried the wrong published date. None of these felt like they were pulling their weight compared to a proper webmention from a blog or Mastodon.

    • 8 min read • 4 interactions

Five AIs and the World Cup

2 posts · June 2026

  1. Five AIs predict the World Cup

    The World Cup rolls around every four years, and with it the office sweepstake, the ritual where everyone gets handed a team at random and pretends to care how it does. I have no interest in football (I had to read a beginner’s guide to how it all works just to follow what’s going on), and I definitely don’t know enough to make an informed prediction of my own. So I thought I’d hand the job to five AI models instead, get them to commit their predictions in public, and keep score. The result is AIWC26, a page that shows every group-stage prediction from five models side by side, scores them against the real results as they come in, and ranks the models in a league table. It looks like Ceefax, because of course it does.

    • 8 min read • 3 interactions
  2. How the five AIs actually did

    The Group Stage of the 2026 World Cup finished today, so it’s time to mark the homework. Back in the launch post five AI models, a deterministic lookup table, and one football fan predicted the same 72 fixtures before a ball was kicked. I’ve been scoring the results on AIWC26 each day as they came in. I’ve got no real opinion on the teams; I’m interested in what the scoring reveals about the models.

    • 12 min read • 1 interaction

ClockworkPi handhelds

3 posts · July 2021 to March 2026

  1. GameShell Stuff

    I bought a ClockworkPi GameShell back in October 2019. I’ve been playing with it a bit lately (in anticipation of the ClockworkPi DevTerm shipping!) and I realised I haven’t yet posted anything about it on here. Here’s some of the hardware mods I’ve made to my GameShell.

    • 3 min read • 1 interaction
  2. DevTerm Stuff

    I helped crowdfund the DevTerm from ClockworkPi back in November 2020 and it arrived in January 2022. It’s a nice device, but it’s not perfect. Here’s some of the mods I’ve made to improve my DevTerm.

    • 2 min read • 4 interactions
  3. I turned my DevTerm into a Game Boy Camera and Printer

    The Game Boy Camera was brilliant and stupid in equal measure, a 128x112 pixel sensor bolted on top of a cartridge, printing to thermal paper on a Game Boy Printer the size of a brick. I got both for Christmas when I was 12 and somehow spent hours with them. The minigames are absurd, the image quality is aggressively awful, and the whole thing looks like a prop from a cheap sci-fi film. I loved it.

    • 9 min read • 5 interactions

The Mangmi Air X

2 posts · November 2025 to December 2025

  1. The Mangmi Air X

    Back in 2015 when I reviewed devices like the GPD XD and similar Android handhelds, the whole ecosystem felt half-baked. Emulation wasn’t particularly good yet, the devices came from obscure Chinese manufacturers, the ROMs were loaded with questionable apps, and the SOCs couldn’t handle much.

    • 7 min read
  2. Exploring OTA firmware updates on the Mangmi Air X

    After reviewing the Mangmi Air X, I got curious about how the firmware updates worked. Mangmi have been pushing updates fairly regularly, which is great to see on a budget device. Officially they release full update packages via Google Drive links, but those have a tendency to disappear over time. I wanted to access the OTA update packages directly and back them up to archive.org alongside the original firmware.

    • 6 min read

Pocket CHIP

4 posts · January 2021 to November 2021

  1. Using ORCA with FluidSynth on the Pocket CHIP

    This post is about using the ORCA livecoding playground on the Pocket CHIP From the ORCA wiki: Orca is a two-dimensional esoteric programming language in which every letter of the alphabet is an operator, where lowercase letters operate on bang, uppercase letters operate each frame. This livecoding language is designed to procedurally generate MIDI, UDP or OSC messages.

    • 2 min read
  2. CHIP Stuff

    This page is a changing document. I’ll probably add more notes to it in the future. These are just some notes that I’ve put together in the past about flashing and upgrading the CHIP (and Pocket CHIP) computer.

    • 4 min read
  3. Adding Global Volume Control to the Pocket CHIP

    I’ve been gradually adding internal speakers to each of my Pocket CHIPs lately and found it cumbersome to have to either open alsamixer or use the volume control page in the Pocket CHIP launcher to control volume.

    • 3 min read • 4 interactions
  4. Adding an internal speaker to the Pocket CHIP

    The Pocket CHIP doesn’t come with internal speakers. It has the GPIO pins for audio, but nothing is hooked up. On the CHIP itself there is a 3.5mm audio jack for connecting headphones, and you can pair a Bluetooth speaker. On the old NTC blog there was a post back in 2016 about adding an internal speaker using a fairly inexpensive PAM8403 amplifier and a speaker. You can find a mirror of the old post from the NTC blog on archive.org

    • 3 min read • 2 interactions

Making a PICO-8 game

5 posts · August 2018 to February 2019

  1. A Dialog System

    This is a flexible dialog system I’ve come up with. It has the following features: automatically wrap long lines of text automatically page super long text to additional dialogs delay control per-letter display First, here are some of the helper functions that I’m using… -- draw a box with a frame -- arguments: -- x,y position -- w,h dimensions -- p padding -- fg,bg foreground and background colors function box(x,y,w,h,p,fg,bg) rectfill(x,y,x+w,y+h,bg) rect(x+p,y+p,x+w-p-1,y+h-p-1,fg) end -- joins two tables function jt(table1,table2) local new = {} for k,v in pairs(table1) do new[k] = v end for k,v in pairs(table2) do new[k] = v end return new end -- sleep for a set number of seconds (using 30fps mode) function sleep(s) for i=1,s*30 do flip() end end And lastly, the meat of the dialog system. I’ve scattered some comments through-out the code to try to explain what I’m doing. I’ll provide some initialisation examples after… function dialog(d) d = d or {} -- defaults _d = { a = true, bg = 5, c = false, dl = 5, fg = 7, p = 8, t = true, ts = 0.05, txt = "", w = 128, y = 96 } -- slightly dynamic defaults _d.x = (128 / 2) - (_d.w / 2) _d.h = 128 - _d.y -- merge passed options with default _d = jt(_d, d) -- text _ch = 6 -- char height _wi = _d.w - (_d.p * 2) _wrap = (_wi / 4) - 1 -- 27 local lines = {} local limit = ceil(#_d.txt / _wrap) local _tb = _d.txt while limit > 0 do local substr = sub(_tb, 1, _wrap) -- grab first bit -- ensure line doesn't have a space at the start if sub(substr,1,1) == " " then substr = sub(substr, 2) end add(lines, substr) -- stick it in our lines table _tb = sub(_tb, _wrap + 1, -1) -- get rest of text limit -= 1 end -- lines limit _ll = flr((_d.h - _d.p - _ch) / _ch) if _d.t == true then -- type it out box(_d.x, _d.y, _d.w, _d.h, _d.p / 2, _d.fg, _d.bg) for l=1,_ll do local _l = lines[l] or "" -- vertical alignment local va = _d.y + _d.p + ((l - 1) * 6) local _lld = _ll while _lld > 1 do if #lines < _lld then va += (_ch / 2) end _lld -= 1 end -- center it if necessary local ha = _d.x + _d.p if _d.c == true then ha = _d.x + (_d.w / 2) - (#_l * 2) end local chars = {} local limit = #_l local ci = 0 -- split line in to chars while limit > 0 do local _ch = sub(_l, 1, ci + 1) add(chars, _ch) ci += 1 limit -= 1 end for c=1,#chars do print(chars[c], ha, va, _d.fg) if _d.a == true then sfx(0) end sleep(_d.ts) -- tiny delay end end else -- print normally box(_d.x, _d.y, _d.w, _d.h, _d.p / 2, _d.fg, _d.bg) for l=1,_ll do local _l = lines[l] or "" -- vertical alignment local va = _d.y + _d.p + ((l - 1) * 6) local _lld = _ll while _lld > 1 do if #lines < _lld then va += (_ch / 2) end _lld -= 1 end -- center it if necessary local ha = _d.x + _d.p if _d.c == true then ha = _d.x + (_d.w / 2) - (#_l * 2) end print(_l, ha, va, _d.fg) if _d.a == true then sfx(0) end end end if #lines > _ll then local _nl = lines -- delete strings we've used for i=1,_ll do del(_nl,lines[1]) end -- join thr remaining local str = "" if #_nl > 1 then for i=1,#_nl do str = str .. _nl[i] end else str = _nl[1] end sleep(_d.dl) -- delay before next -- call recursive local n = jt(_d, {txt=str}) dialog(n) else sleep(_d.dl) -- delay before next end end Whew that was a lot of code. Here’s how you can use it: -- short, centered, delay of 2 dialog({ txt = "hello,", c = true, dl = 2 }) -- short, centered dialog({ txt = "my name is inigo montoya", c = true }) -- short, centered dialog({ txt = "you killed my father", c = true }) -- short, centered, not typed dialog({ txt = "prepare to die!", c = true, t= false }) -- long, typed, multiple message boxes dialog({txt="this is a lot of text that will wrap on to multiple lines and hopefully use a recursive message box in order to show all of the text ♥"}) You can see how this all looks below:

    • 4 min read
  2. A custom font

    Out of the box Pico-8 has one font. It’s the utilitarian answer to most problems. 3px by 5px and fairly legible. This font isn’t always good enough. How about making your own font? I’ve seen lots of approaches for creating fonts in Pico-8 – mostly using sprites for each glyph of the font, but then you’re limited to 8x8px and a have to mess with pal modes if you want to adjust colors on-the-fly. My approach uses the raw pixel data encoded in strings that are rendered using calls to pset, falling back to the built-in font for any missing glyphs. First, here are my helper functions… -- merge tables function mt(t1,t2) local new = {} for k,v in pairs(t1) do new[k] = v end for k,v in pairs(t2) do new[k] = v end return new end -- less verbose way to do ternary operations function ternary(cond ,t ,f ) if cond then return t else return f end end I had to come up with a compact way to represent the glyphs. Initially I used something like: { glyph = "o", pixels = { 0,1,1,1,0, 1,0,0,0,1, 1,0,0,0,1, 1,0,0,0,1, 1,0,0,0,1, 1,0,0,0,1, 0,1,1,1,0, }} But that contains a lot of tokens and lots of repeated/dead values to represent the empty pixels in a glyph. Eventually I moved to something like: {"o", "0.1.3.1.1.3.2.3.2.3.2.3.2.3.1.1.3"} That’s the same pixel data, but encoded in to a much shorter string. The first number indicates our starting parity – this can be 0 or 1 and tells us if the top/left pixel is empty or filled. Each time we have a . the parity flips, so with the example of our “o” glyph it starts with 0 for 1 count, then 1 for 3 counts, 0 for 1 count, 1 for 1 count, etc. When the remaining pixels in a glyph are 0 we can just omit them from the glyph data as they’re not drawn. The real wins are in the more simple/empty glyphs, such as “.” for example. Before: { glyph = ".", pixels = { 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,1,0,0, }} After: {".", "0.32.1"} Making this change took me from an initial token count of roughly 2800 to a much more manageable count of 560-ish. So with that technical talk out of the way, here’s the complete glyph set in code, and the function to render text. I’ll give some usage examples after. global_y = 0 function write(str,opts) opts = opts or {} _opts = { x=0, y=global_y, c=7 } opts = mt(_opts,opts) font = { w=5, h=7, -- first num indicates starting value, 0 or 1 -- each number indicates number of 0 or 1 in a row -- flips at each . glyphs = { {"0", "0.1.3.1.1.3.2.3.2.3.2.3.2.3.1.1.3"}, {"1", "0.2.1.3.2.2.1.1.1.4.1.4.1.4.1.2.5"}, {"2", "0.1.3.1.1.3.1.4.1.3.1.3.1.3.1.3.5"}, {"3", "0.1.3.1.1.3.1.4.1.2.2.5.2.3.1.1.3"}, {"4", "0.3.2.2.1.1.1.1.1.2.2.3.6.4.1.4.1"}, {"5", "1.6.4.1.5.3.5.2.3.1.1.3"}, {"6", "0.1.3.1.1.3.2.4.4.1.1.3.2.3.1.1.3"}, {"7", "1.5.4.1.4.1.3.1.3.1.3.1.3.1"}, {"8", "0.1.3.1.1.3.2.3.1.1.3.1.1.3.2.3.1.1.3"}, {"9", "0.1.3.1.1.3.2.3.1.1.4.4.2.3.1.1.3"}, {"a", "0.1.3.1.1.3.2.3.7.3.2.3.2.3.1"}, {"b", "1.4.1.1.3.2.3.5.1.1.3.2.3.5"}, {"c", "0.1.3.1.1.3.2.4.1.4.1.4.1.3.1.1.3.1"}, {"d", "1.4.1.1.3.2.3.2.3.2.3.2.3.5"}, {"e", "1.6.4.1.4.4.1.1.4.1.4.5"}, {"f", "1.6.4.1.4.4.1.1.4.1.4.1"}, {"g", "0.1.3.1.1.3.2.3.2.4.1.1.4.3.1.1.3"}, {"h", "1.1.3.2.3.2.3.7.3.2.3.2.3.1"}, {"i", "1.5.2.1.4.1.4.1.4.1.4.1.2.5"}, {"j", "1.5.2.1.4.1.4.1.2.1.1.1.2.1.1.1.3.2"}, {"k", "1.1.3.2.2.1.1.1.1.1.2.2.3.1.1.1.2.1.2.1.1.1.3.1"}, {"l", "1.1.4.1.4.1.4.1.4.1.4.1.4.5"}, {"m", "1.1.3.3.1.3.1.1.1.2.3.2.3.2.3.2.3.1"}, {"n", "1.1.3.3.2.2.1.1.1.2.2.3.3.2.3.2.3.1"}, {"o", "0.1.3.1.1.3.2.3.2.3.2.3.2.3.1.1.3"}, {"p", "1.4.1.1.3.2.3.5.1.1.4.1.4.1"}, {"q", "0.1.3.1.1.3.2.3.2.3.2.3.1.1.3.5.1"}, {"r", "1.4.1.1.3.2.3.5.1.1.1.1.2.1.2.1.1.1.3.1"}, {"s", "0.1.3.1.1.3.2.5.3.5.2.3.1.1.3"}, {"t", "1.5.2.1.4.1.4.1.4.1.4.1.4.1"}, {"u", "1.1.3.2.3.2.3.2.3.2.3.2.3.1.1.3"}, {"v", "1.1.3.2.3.1.1.1.1.1.2.1.1.1.2.1.1.1.3.1.4.1"}, {"w", "1.1.3.2.3.2.3.2.1.1.1.2.1.1.1.2.1.1.1.1.1.1.1.1"}, {"x", "1.1.3.2.3.1.1.1.1.1.3.1.3.1.1.1.1.1.3.2.3.1"}, {"y", "1.1.3.1.1.1.1.1.3.1.4.1.4.1.4.1.4.1"}, {"z", "1.5.4.1.3.1.3.1.3.1.3.1.4.5"}, {" ", "0.35"}, {".", "0.32.1"}, {",", "0.27.1.3.1"}, {"!", "0.2.1.4.1.4.1.4.1.4.1.9.1"}, {"?", "0.1.3.1.1.3.1.4.1.3.1.3.1.9.1"}, {"'", "0.3.1.3.1"}, {"\"", "0.1.1.1.1.1.1.1.1"}, {":", "0.12.1.14.1"}, {";", "0.12.1.14.1.3.1"}, {"(", "0.3.1.3.1.3.1.4.1.4.1.5.1.5.1"}, {")", "0.1.1.5.1.5.1.4.1.4.1.3.1.3.1"}, {"[", "0.1.3.2.1.4.1.4.1.4.1.4.1.4.3"}, {"]", "0.1.3.4.1.4.1.4.1.4.1.4.1.2.3"}, {"{", "0.2.2.3.1.4.1.3.2.4.1.4.1.4.2"}, {"}", "0.1.2.4.1.4.1.4.2.3.1.4.1.3.2"}, {"<", "0.3.1.3.1.3.1.3.1.5.1.5.1.5.1"}, {">", "0.1.1.5.1.5.1.5.1.3.1.3.1.3.1"}, {"-", "0.16.3"}, {"+", "0.12.1.3.3.3.1"}, {"/", "0.4.1.3.1.4.1.3.1.3.1.4.1.3.1"}, {"\\", "1.1.5.1.4.1.5.1.5.1.4.1.5.1"}, } } _x = opts.x _y = opts.y letters = {} for i=1,#str do letter = sub(str,i,i) add(letters,letter) end for k,v in pairs(letters) do letter = nil for _glyph in all(font.glyphs) do if _glyph[1] == v then letter = _glyph[2] end end local _sx = _x local _sy = _y if letter != nil then local _px = _sx local _py = _sy pixels = {} local _p = sub(letter,1,1) local prev = nil for i=2,#letter do cur = sub(letter,i,i) if type(prev) == "number" and cur != "." then cur = sub(letter, i-1, i) end if cur == "." then _p = ternary(_p == "0", "1", "0") else for j=1,tonum(cur) do add(pixels, _p) end end prev = tonum(cur) end for i,p in pairs(pixels) do i = i - 1 if i % font.w == 0 then _py += 1 _sx = _x - (font.w * (i / font.w)) end _px = _sx + i if p == "0" then pset(_px, _py, opts.c) end end else font.w = 8 print(v, _sx, _y + ((font.h - 2)/2), opts.c) end _x += font.w + 1 end global_y += font.h + 2 end If you’re still with me, congratulations! Here is how you use the function: function _draw() cls() global_y = 0 -- reset on each draw call write("hello world") -- passing a color is possible! write("this is max 🐱", {c=12}) write("my font brings all") write("the devs to the yard") write("0123456789") write("abcdefghijklm") write("nopqrstuvwxyz") write(".,?!:;()[]{}-+/\\<>") write("yes, this is a lot") write("nicer to read! ♥") write("") write("█▒🐱⬇️░✽●♥☉웃⌂⬅️😐") write("♪🅾️◆…➡️★⧗⬆️ˇ∧❎▤▥") write("@#$%^&*") end And this is how it looks:

    • 5 min read
  3. A menu and state system

    I’ve been playing with state systems lately, and decided to build a menu system to play with going between each state. First of all, my helper functions… I’m using this to print outlined text: -- print text with an outline function printo(str,x,y,c1,c2) for xoff=-1,1 do for yoff=-1,1 do print(str,x+xoff,y+yoff,c1) end end print(str,x,y,c2) end The menu function has two modes, one for running in _update() and one for running in _draw(), you control this by passing "u" or "d" as the second argument. The first agument takes a table containing a menu definition. I’ll talk more about this with an example after the wall of code: function menu(m, mode) local ic=#m.items if mode == "u" then if btnp(2) then -- up m.selection -= 1 if m.selection < 1 then m.selection = 1 end end if btnp(3) then -- down m.selection += 1 if m.selection > ic then m.selection = ic end end if btnp(5) then -- x if m.items[m.selection][2] then m.items[m.selection][2]() end end end if mode == "d" then local padding={4,4,0,6}-- t/r/b/l local lineheight=8 -- find longest string local widest_str=0 for i,item in pairs(m.items) do if #item[1] > widest_str then widest_str = #item[1] end end local width = (widest_str*4)+padding[2]+padding[4] local height = (ic*lineheight)+padding[1]+padding[3] local xoff = 64 - (width/2) local yoff = 64 - (height/2) local x0 = xoff local y0 = yoff local x1 = xoff + width local y1 = yoff + height -- box rect(x0-2,y0+1,x1+2,y1+2, 5) rect(x0-1,y0-1,x1+1,y1+1,0) rectfill(x0,y0,x1,y1,1) rect(x0,y0,x1,y1,7) -- aditional text if m.guide then local _w = #m.guide*4 local _x = 64 - (_w/2) local _y = y1+padding[1]+2 printo(m.guide,_x,_y, 0, 7) end -- items for i,item in pairs(m.items) do if i == m.selection then -- selection highlight printo( item[1], xoff+padding[4], yoff+padding[1]+((i-1)*lineheight), 0,7 ) if m.items[m.selection][2] then palt(0,false) palt(1,true) local xadj=(5 - time()%3) spr(1,x0-xadj,y0+padding[1]+((i-1)*lineheight) - 1) pal() end else -- normal item printo( item[1], xoff+padding[4], yoff+padding[1]+((i-1)*lineheight), 0,6 ) end end end end There’s a lot going on there, but it’s basically maintain a counter for the user’s current selection, and then if there is a function associated with a menu item call it when the x button is pressed. Here’s how the menu definitions look _menu_start = { selection = 1, items = { {"new game", game}, {"new game", game}, {"new game", game}, {"new game", game}, {"credits", credits} }, guide="❎ to select" } _menu_game = { selection = 1, items = { {"game over", die}, {"congratulations", win}, {"---"}, {"back", start} } } _menu_credits = { selection = 1, items = { {"this sub menu contains lots of"}, {"strings without actions bound "}, {"to them, so you can just show "}, {"multiple lines of text."}, {"30 characters is a rough limit"}, {"---"}, {"back", start} }, guide="enjoy ♥" } You can go between menus using a state system. Here’s the barebones of a state system. We assign a new function to _upd() and _drw() each time the state changes. function _init() start() -- defined later end --update function _update() _upd() end function update_start() menu(_menu_start, "u") end function update_game() menu(_menu_game, "u") end function update_credits() menu(_menu_credits, "u") end --draw function _draw() cls(13) _drw() end function draw_start() menu(_menu_start, "d") end function draw_game() menu(_menu_game, "d") end function draw_credits() menu(_menu_credits, "d") end -- functions that are called directly -- through menu selections function start() _upd=update_start _drw=draw_start end function game() _upd=update_game _drw=draw_game end function credits() _upd=update_credits _drw=draw_credits end And here’s how it looks in action. I’ve got a custom sprite for the selection hand drawn in sprite slot 1:

    • 3 min read
  4. Outlining text and sprites

    It’s pretty simple to create an outlined text effect with a simple function, such as: function printo(text,x,y,text_color,border_color) local text_color = text_color or 7 local border_color = border_color or 0 -- render the "outline" in each direction for _x=-1,1 do for _y=-1,1 do print(text,x+_x,y+_y,border_color) end end -- print the foreground text print(text,x,y,text_color) end -- usage: printo("Hello world!",64,64,12,8) The process for outlining a sprite is very similar, but it uses the pal() function to change colors and works best of all if your sprite is a single color… -- where spr_table is like: -- {spr_index, base_color} function spro(spr_table,x,y,spr_color,border_color) local spr_color = spr_color or 7 local border_color = border_color or 0 -- render the "outline" in each direction for _x=-1,1 do for _y=-1,1 do -- replace the base_color with new border_color pal(spr_table[2],border_color) -- draw the sprite with the new color spr(spr_table[1],x+_x,y+_y) end end -- replace the base_color with the new spr_color pal(spr_table[2],spr_color) -- render the recolored sprite spr(spr_table[1],x,y) -- reset the rendering palette pal() end -- usage: spro({1,7},64,64,12,8) And obviously you can set this spro function up to handle more of the properties available to spr, such as number of tiles in x/y, and whether to flip or not. As a final solution if you’re going to be doing this a lot you could combine the previous two functions to one mega outline function: function outline(what,x,y,fc,bc) if typeof(what) == "string" then printo(what,x,y,fc,bc) else spro(what,x,y,fc,bc) end end And here’s how the final effect looks when applied to a 1-color sprite and some text, with random colors:

    • 2 min read
  5. Approaches for a simple state system

    There are lots of ways to achieve even the most simple goal in programming, and Pico-8 – even with it’s limited nature – is no different to any other system. Today I’m looking at state systems. There are lots of approaches to handling state out there, and each has their own pros and cons. In Pico-8 the main concern is keeping the token count low so that you can add more features to your game. Beginner state system This first state system uses a state variable and an enum table of available states to decide which state we’re currently in. The state variable is updated by user input (button press, or death) and it’s contents controls which functions are executed in the _update() and _draw() functions. If you’ve got a huge game the conditions in the _update() and _draw() functions can quickly become unweildly. The main benefit here is that you can control what to execute in the _update() and _draw() functions from one place. This approach is quite token hungry. function _init() t = 0 -- create timer state = 1 states = { menu = 1, game = 2, gameover = 3 } hp = 100 end function _update() t += 1 -- increment timer if state == states.menu then update_menu() elseif state == states.game then update_game() else update_gameover() end end function _draw() if state == states.menu then draw_menu() elseif state == states.game then draw_game() else draw_gameover() end end function update_menu() if btn(4) and btn(5) then state = states.game end end function draw_menu() print("Press z and x to start") end function update_game() if hp < 1 then state = states.gameover end end function draw_game() print("HP: "..hp) end function update_gameover() if btn(4) and btn(5) then state = states.game end end function draw_gameover() print("Press z and x to start over") end Improved state system This is the approach I prefer, the _update() and _draw() functions call a variable that points to another function. To change state you simply update this variable. There is no variable or enum needed to keep track of state, but you do need to explicitly redeclare the _upd and _drw variables each time you change state. This is approach is quite light on the token count. -- core init/update/draw functions function _init() t = 0 -- create timer hp = 100 _upd = upd_menu _drw = drw_menu end function _update() t += 1 -- increment timer _upd() end function _draw() cls() _drw() end -- menu update/draw functions function upd_menu() if btn(4) and btn(5) then _upd = upd_game _drw = drw_game end end function drw_menu() print("Press z and x to start") end -- game update/draw functions function upd_game() if hp < 1 then _upd = upd_gameover _drw = drw_gameover end end function drw_game() print("HP: "..hp) end -- gameover update/draw functions function upd_gameover() if btn(4) and btn(5) then _upd = upd_game _drw = drw_game end end function drw_gameover() print("Press z and x to start over") end

    • 3 min read

Cardboctober

32 posts · September 2016 to October 2016 · See all the days