A list of helpful libraries and code

Thanks Nick. I had done an Underscore:new() but forgot the magic step 3. Sorry! That'll teach me to post in a hurry.

Underscore has proved very useful for me to reduce code manipulating tables. For example my code checking valid combinations/patterns is much reduced. Really elegant!

1 Like

Well, today I learned about method chaining because of this post! Very cool!

3 Likes

This is really unintuitive but import already returns the return value of the imported script, but-but imports only work at the beginning of a line, but-but-but, since Lua doesn't really care about whitespace, you can write it like this:

local __ =
import 'underscore'

:sweat_smile:

__.each({1, 2, 3}, print)
__({1, 2, 3}):each(print)

Both examples now work as expected with an unmodified underscore.lua using this method.

11 Likes

Oh — thanks! I'll never stop learning.

I absolutely love how these chain together. Thank you for sharing this!

I have one question that might be dumb — and is basically academic, because it's not a big deal: would it be possible to write this in a way that wouldn't require sequence.update() to be called every frame? Can the computation of the value just be done when get() is called?

Oh… now that I see you've added callbacks, I guess those wouldn't work properly if you didn't have .update() called every frame.

not your fault @nick_splendorr but I had to stop using that round code as it would often spit out 0.09999999 rather than 0.1 which was tripping up a bunch of telemetry readouts in my game.

i now use the one below, it's from https://stackoverflow.com/a/57299034/28290 but slightly modified to work with the same syntax as the one you posted (no code changes!):

function math.round(number, precision)
   local fmtStr = string.format('%%0.%sf',precision)
   return string.format(fmtStr,number)
end
2 Likes

Yeah it was already like that before the callbacks because that's a design pattern that I like. An update function is called once and all sequence object will be based on that. With just .get() you might actually have different result depending when it's called in the code because the timing is slightly different. So you might end up with two sprites using the same sequence but get slightly different results which mean they might be off by few pixels.

But that's true it would have been a simpler design to just have a getter which is also extremely valuable.

1 Like

string manipulation in lua can be a costly operation so I would recommend using this round function only when you really need the precision.

Good point! I'm only using it in debug display. But I'll try to find a faster one.

@Nic what do you think about profiling each lua command on the Playdate hardware to give us an idea about how they perform. Maybe somebody has already done this for Lua?

I don't recall seeing any lua function benchmark. That could be useful but also a lot of work I guess (I will definitively not do that :sweat_smile:)

But for performance this lua documentation is always a good reference and in this case how string are handheld in lua.

I'll try the beginnings of a function benchmark.

I've read that doc and got some nice improvements!

I wanted to revisit the rounding function. So I write one that is less susceptible to float precision errors. After some testing I didn't encounter a single precision issue.

function math.round(number, precision)
	local p = math.pow(10,precision)
	local half = (number >= 0 and 0.5) or -0.5
	return (number*p+half)//1/p
end

But I also benchmarked the different versions and to my surprise the string version is definitively not as bad as I would have thought. It is slower but it is still close enough to use without concerns IMO. I also run the benchmark code from within a bigger codebase with more strings and I still similar results.
Screenshot 2020-06-14 at 11.30.03

Update:
It should be noted also that the round function used by Nick is actually much more flexible. So I rewrote a new one to keep it precise and flexible and also twice as fast as before because I like this type of pointless exercise

function math.round( number, bracket )
	bracket = bracket or 1
	
	-- path for additional precision 
	if bracket<1 then
		bracket = 1//bracket
		local half = (number >= 0 and 0.5) or -0.5
		return (number*bracket+half)//1/bracket
	end

	local half = (number >= 0 and bracket/2) or -bracket/2
	return ((number+half)//bracket)*bracket
end
Rounding Benchmark - 500000 calls
string > 430 ms
splendorr > 160 ms
nic (new) > 77 ms
nic > 145 ms
7 Likes

Love you, Nic!

I will get to start the benchmarking suite this week.

This is such a common function I wonder if it would make sense to have a version written in C added to the math table. @dan ?

Sounds like it would be a good addition! Nobody's written a C version yet, right? I'd be curious to benchmark the speed difference.

This is great! I appreciate seeing all these different approaches to the same problem. Using // instead of math.floor, and Lua's short-circuit comparators instead of math.sign, are all smart changes. I no idea you could just use -bracket as an equivalent to -1 * bracket, though! I think there are... about a hundred places in my code I code make that change :sweat_smile: :playdate_crying:

I'm switching over to this newest version in my code, and will test to make sure it works everywhere as expected! Thanks as always for your expertise, @Nic! <3

1 Like

Hey @dustin! Hope all your stuff is going well! I just tried the Signal and State libraries for the first time, and while they appear to mostly-work out of the box, I had some trouble with the State subscription functions. I worked out that they're receiving more arguments than the example suggested; where you have function(old_value, new_value), the function is actually receiving nil(not sure where this is coming from?), key, old_value, new_value. Could you tell me if that's expected, or if I've hooked up something wrong? It's usable as-is, but I'll just need to call the function with (_, _, old_value, new_value).

Anyway, here's a little project that illustrates the issue: press left or right to increment or decrement the number GameState.wall (because I'm tracking which wall I have selected in our game), with console output printing the (...) args passed into the subscribed function.

splendorr-state-test.zip (15.3 KB)

And the main.lua for reference, which just imports the Signal and State libraries as written in your post above:

import 'CoreLibs/graphics'
import 'Signal'
import 'State'

GameState = State()
GameState.wall = 1
print('GameState.wall starts at '.. tostring(GameState.wall))

local gfx = playdate.graphics

function playdate.update()
  gfx.clear()
  if playdate.buttonJustPressed('left') then
    print('Pressed Left! Subtracting 1')
    GameState.wall = GameState.wall - 1
  end
  if playdate.buttonJustPressed('right') then
    print('Pressed Right! Adding 1')
    GameState.wall = GameState.wall + 1
  end

  gfx.drawTextAligned(GameState.wall, 200, 120, kTextAlignment.center)
end

--[[ The provided example has 2 args:
GameState:subscribe("score", self, function(old_value, new_value) end
but the usage below receives 4, which are: unknown, key, old_value, new_value
My question is, what's the first nil (implicit self?) and is the key expected, even though the example doesn't have it?
]]--

GameState:subscribe('wall', self, function(...)
  print('Subscription args:')
  print(...)
  print('GameState.wall is '.. tostring(GameState.wall))
end)

And here's the console output, running the game and then pressing right twice:

GameState.wall starts at 1
Pressed Right! Adding 1
Subscription args:
nil	wall	1	2
GameState.wall is 2
Pressed Right! Adding 1
Subscription args:
nil	wall	2	3
GameState.wall is 3

Thanks!

So looking at your code, you're passing in self which is undefined in this context. I wanted to allow you to use this nicely within the context of an object where you would want self bound to your object. Lua passes self as the first argument.

I think I need to detect if the context is nil then call the function without it so this would work as expected. Also I will change it where subscribe accepts just key and fn (no bind arg) so you can use this in a global context.

Thanks for the report!

1 Like