Screenshots
Attempting to design an app icon for a Chips Challenge fan game I’m working on. Going for something that looks like the fireball sprite in the original game with a hint more realism and tinted in the colour blue. For reference, here’s the original fireball sprite:
And here’s my attempt:
I started with Stable Diffusion to get the base image:
Then imported into Acorn to rotate it, colourise it, and distort it to look a bit closer to the original sprite.
Desaturating the original image got rid of the purple centre, then applying the Glowhoo and Hue Adjust effect recolourised it to the blue I was looking for (I’m not sure what the Glowhoo effect does, but it seems to adjust the colour based on the pixel intensity, so it was good enough for what I wanted). Finally, I added a Twirl Distortion effect to achieve the slight warp in the star.
And yeah, it’s not going to win any design awards, but it’s good enough for now.
Oh, and just for kicks, here was my first attempt of producing the sprite using Affinity Designer.
That’s definitely not going to win any design awards. π
For reasons that are “totally” coincidental to the news of the day, I had a quick check to see how much the domain Y would cost:
Hmm, might be a bit much for a joke domain. π
A really nice quality of life improvement you can make in Terminal.app: map Option-Backspace to ^W
(Control-W, or \027
) so that pressing it in the shell will delete one word to the left, like most other MacOS apps:
TIL you can enter a photo description in Google Photos. Select a photo, click the Info icon, and a free-text “description” field is revealed. Not super sure what the description is to be used for, but I’m hoping to use it for photo captions.
I’ve been working on Micropub Checkins over the last week. It’s been a bit of a rush trying to get it into a usable state for an upcoming trip. And by “usable”, I mean a form that I can tolerate, and when it comes to projects like this, I can tolerate quite a lot. It can have a really dodgy UI (which this does) and miss some really important features that are annoying to work around; but if it works, and doesn’t loose data, I’ll be fine with it.
The last week was dedicated to making the act of checking in distinct from publishing it. Until now, check-ins were published the minute they were entered, meaning that you cannot check-in somewhere unless you’re comfortable with people knowing where you are the minute you do. Yes, some people like it that way, but not me. And I’m aware that this’ll only be the case if people are following my check-in blog, which I’m doubtful of.
So pressing the floating action button and choosing a check-in type now starts the flow of a new check-in that will get saved in an SQLite database. You can edit the check-in whenever you like, so long as it’s not published. Currently there’s no real way of deleting a check-in unless it’s been published. This is a bit dodgy, but it’s a good example of how tolerant I am with working around these feature gaps for the moment.
Check-ins can be published by tapping the upward facing button on the main screen. Any check-in with a lock is private and will not be published until you toggle the “Ready to publish” switch in the properties. Doing so will not change the date of the check-in: it will still have the date and time that check-in was created.
On the subject of publishing, I had some issues with Dart’s date and time methods. The method on the DateTime class used to produce an ISO-8501 date-stamp don’t include the time-zone if the date and time is not in UTC. This is important as I want the post date and time to be as close to the check-in time as possible, and in the time-zone of the phone. DateTime knows all this, including what the time-zone we’re in. So why didn’t the developers include it in the ISO-8501 date-time string?
This is really strange. Fortunately, ChatGPT stepped in to help out, writing a function which will add the time-zone offset to the ISO-8501 date-time string:
String formatTimeZoneOffset(Duration offset) {
String sign = offset.isNegative ? '-' : '+';
int hours = offset.inHours.abs();
int minutes = (offset.inMinutes.abs() % 60);
return '$sign${_padZero(hours)}:${_padZero(minutes)}';
}
Honestly, ChatGPT has been so helpful over the past week with this project, I probably should give it a credit if I get this polished enough to release.
I got a little bored today so I added task progress indicators to this Obsidian roadmap thing I built for work.
When a task is created, but is not yet scheduled, it appears as an outlined rectangle. It turns into a grey rectangle when it’s added to the sprint. It then changes colour as the task progresses through the software lifecycle; turning purple while it’s being developed, blue while it’s being tested, and finally green when it’s ready for release.
They’re implemented as embedded SVG images, added directly to the note much like the span element used for status labels.
It’s nice being able to add embellishments like this. Obsidian treating notes as regular files on the file-system is a huge advantage for these sorts of automations. No need to learn how to make a plugin; just write a shell script1 that’ll output Markdown, schedule it to run a couple of times a day, and you’re good to go.
Back working on Micropub Checkin. Re-engineered the home page to now include a list of what would eventually be check-ins β both historical and soon to be published β complete with the check-in type emoji as the icon:
The same list of emoji icons now adorn the check-in type picker as well (except for the airplane one which seems to always be shown as what I can only describe as the βWingdingβ representation):
I went around a bit trying to work out how best to use these emojis icons in the leading
slot of the ListTile
widget. I expored trying to convert them to IconData
, but it turns out just using a Text
widget with a large font worked well. I wrapped in in a Widget
type with a fixed font-size and so far it looks quite good, at least in the emulator:
class EmojiIcon extends StatelessWidget {
final String emoji;
const EmojiIcon({super.key, required this.emoji});
Widget build(BuildContext context) {
return Text(emoji, style: TextStyle(fontSize: 26.0));
}
}
Also started working on a Cubit to handle state for the main page. I had a bit of trouble working ont where the soon-to-be database call to get the list of checkins should go in the cubit. After asking ChatGPT, it looks like the initializer is the best place for it:
class CheckinListCubit extends Cubit<CheckinListState> {
CheckinListCubit(): super(LoadingCheckinListState()) {
loadCheckinList();
}
void loadCheckinList() async {
var listOfCheckins = await read_database();
emit(FoundCheckinListState(checkins));
}
}
Iβve got some scaffolding code in place to simulate this, and so far it seems to work.
I need to start working on the database layer and having the ability to edit and delete check-ins before they’re published. I think I’ll tackle that next.
Does this loading window really need to be modal?
Hmm, either Daniel is super obsessed with the Beths (or at least posting about it), or there’s a bug somewhere. π
Building F5 To Run
At the risk of talking about something that I’ve only just started, I’d thought today I write about what I’m working on right now.
I’ve been going through my digital archives this weekend, trying to get it into something more permenant than the portable USB drives it’s currently stored on. Amongst all that stuff is a bunch of QBasic apps and games I wrote way back when I was a kid. Over the years it’s laid dormant but I do like revising them from time to time.
Is it a form of nostalgia? An attempts to live past glories? Maybe? I was pretty proud of them at the time, much like anyone else that’s proud of their early stuff while they’re leaning to code. And I know about the risk of living in the past at the expense of the present. But I also know that if I get rid of them, I’d regret it. I already regret loosing the things from the archive so far, due to bad disks or just missing things while copying them from portable hard-drive to portable hard-drive. I don’t want to loose any more.
So in an act of posterity, I’d figured it’s time to coat them in amber and put them online. So that’s what I’m doing now.
These apps run without issue in DosBox, and hearing about how the Wayback Machine has managed to make a bunch of DOS games playable within the browser, I wondered if I could do something similar. Anything that deals with virtualisation is always going to be a little bit involved. I guess one thing going for these is that they were written for a pretty slow machine and a pretty thin OS that would be trivial for modern hardware to emulate. The apps themselves, even compiled to an EXE file, are not very taxing on the hardware back then either. But I still expected to do a bit of heavy lifting myself.
How wrong I was! After a tiny bit of research β and by tiny I mean one Ecosia search β I managed to find a JavaScript library called JS-Dos which provides a DosBox emulator that’s runnable from a browser. All I need to do is prepare a bundle on what I want to run (more on that below) and with a bit of JavaScript, I can start a DosBox machine in the browser and mount it to a HTML element. The library does all the work.
How To Use JS-Dos
It’s still early days, but here’s what I learnt about using the library so far.
First, the library comes as a NPM package, or can be loaded from their CDN in the form of a <script>
import.
I first tried using the NPM package, but I didn’t know the appropriate import
statement to use, and the documentation was not forthcoming on this front.
So I went with the CDN approach. I’m using Hugo Pipes to fetch the remote JavaScript file and make a local bundle so I can host it from the site itself. It comes with some CSS which I also need to get (note, I’m using parenthesis instead of curly braces here as I’m not sure how to include two curly braces in a code-block).
(( $jsDosCSS := resources.GetRemote "https://js-dos.com/v7/build/releases/latest/js-dos/js-dos.css" ))
<link rel="stylesheet" href="(( $jsDosCSS.RelPermalink ))">
(( $jsDosJS := resources.GetRemote "https://js-dos.com/v7/build/releases/latest/js-dos/js-dos.js" ))
<script src="(( $jsDosJS.RelPermalink ))" defer></script>
I also needed to get an appropriate wdosbox emulator. This comes in the form of a WASM file, plus a small JavaScript file which
I assume is some simple bootstrapper. I’ve downloaded these and stored them in the static/emulators
directory of my Hugo project.
The JSDos library loads them on demand and I needed to set the URL path prefix for these two files so that JSDos knows where to get them:
emulators.pathPrefix = '/emulators/';
Next, I needed to build a bundle. These are the DOS programs that are launched with DosBox. They’re effectively just Zip files holding some metadata, the DOS executable, and any files needed for the program to run. There’s some basic information about how to make them, and there’s even an online tool which will take on a lot of the tedious work. I’ve used it to make a couple of test bundles and it works quite well. I’d like to eventually make my bundles myself but I’ll stick with the tool for the time being, at least until I’ve got a DosBox configuration that I’m happy with. One thing the tool does is give you the ability to define an overlay so that these DOS apps are usable from within a mobile browsers. I’ll see if I can get away from needing these overlays at this stage. I’m not expecting anyone with a mobile app to try these out.
The contents of .jsdos/dosbox.conf for the test bundle `logo-2.jsdos`
[sdl]
autolock=false
fullscreen=false
fulldouble=false
fullresolution=original
windowresolution=original
output=surface
sensitivity=100
waitonerror=true
priority=higher,normal
mapperfile=mapper-jsdos.map
usescancodes=true
vsync=false
[dosbox]
machine=svga_s3
language=
captures=capture
memsize=16
[cpu]
core=auto
cputype=auto
cycles=max
cycleup=10
cycledown=20
[mixer]
nosound=false
rate=44100
blocksize=1024
prebuffer=20
[render]
# frameskip: How many frames DOSBox skips before drawing one.
# aspect: Do aspect correction, if your output method doesn't support scaling this can slow things down!.
# scaler: Scaler used to enlarge/enhance low resolution modes.
# If 'forced' is appended, then the scaler will be used even if the result might not be desired.
# Possible values: none, normal2x, normal3x, advmame2x, advmame3x, advinterp2x, advinterp3x, hq2x, hq3x, 2xsai, super2xsai, supereagle, tv2x, tv3x, rgb2x, rgb3x, scan2x, scan3x.
frameskip=0
aspect=false
scaler=none
[midi]
# mpu401: Type of MPU-401 to emulate.
# Possible values: intelligent, uart, none.
# mididevice: Device that will receive the MIDI data from MPU-401.
# Possible values: default, win32, alsa, oss, coreaudio, coremidi, none.
# midiconfig: Special configuration options for the device driver. This is usually the id of the device you want to use.
# See the README/Manual for more details.
mpu401=intelligent
mididevice=default
midiconfig=
[sblaster]
# sbtype: Type of Soundblaster to emulate. gb is Gameblaster.
# Possible values: sb1, sb2, sbpro1, sbpro2, sb16, gb, none.
# sbbase: The IO address of the soundblaster.
# Possible values: 220, 240, 260, 280, 2a0, 2c0, 2e0, 300.
# irq: The IRQ number of the soundblaster.
# Possible values: 7, 5, 3, 9, 10, 11, 12.
# dma: The DMA number of the soundblaster.
# Possible values: 1, 5, 0, 3, 6, 7.
# hdma: The High DMA number of the soundblaster.
# Possible values: 1, 5, 0, 3, 6, 7.
# sbmixer: Allow the soundblaster mixer to modify the DOSBox mixer.
# oplmode: Type of OPL emulation. On 'auto' the mode is determined by sblaster type. All OPL modes are Adlib-compatible, except for 'cms'.
# Possible values: auto, cms, opl2, dualopl2, opl3, none.
# oplemu: Provider for the OPL emulation. compat might provide better quality (see oplrate as well).
# Possible values: default, compat, fast.
# oplrate: Sample rate of OPL music emulation. Use 49716 for highest quality (set the mixer rate accordingly).
# Possible values: 44100, 49716, 48000, 32000, 22050, 16000, 11025, 8000.
sbtype=sb16
sbbase=220
irq=7
dma=1
hdma=5
sbmixer=true
oplmode=auto
oplemu=default
oplrate=44100
[gus]
# gus: Enable the Gravis Ultrasound emulation.
# gusrate: Sample rate of Ultrasound emulation.
# Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
# gusbase: The IO base address of the Gravis Ultrasound.
# Possible values: 240, 220, 260, 280, 2a0, 2c0, 2e0, 300.
# gusirq: The IRQ number of the Gravis Ultrasound.
# Possible values: 5, 3, 7, 9, 10, 11, 12.
# gusdma: The DMA channel of the Gravis Ultrasound.
# Possible values: 3, 0, 1, 5, 6, 7.
# ultradir: Path to Ultrasound directory. In this directory
# there should be a MIDI directory that contains
# the patch files for GUS playback. Patch sets used
# with Timidity should work fine.
gus=false
gusrate=44100
gusbase=240
gusirq=5
gusdma=3
ultradir=C:\ULTRASND
[speaker]
# pcspeaker: Enable PC-Speaker emulation.
# pcrate: Sample rate of the PC-Speaker sound generation.
# Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
# tandy: Enable Tandy Sound System emulation. For 'auto', emulation is present only if machine is set to 'tandy'.
# Possible values: auto, on, off.
# tandyrate: Sample rate of the Tandy 3-Voice generation.
# Possible values: 44100, 48000, 32000, 22050, 16000, 11025, 8000, 49716.
# disney: Enable Disney Sound Source emulation. (Covox Voice Master and Speech Thing compatible).
pcspeaker=true
pcrate=44100
tandy=auto
tandyrate=44100
disney=true
[joystick]
# joysticktype: Type of joystick to emulate: auto (default), none,
# 2axis (supports two joysticks),
# 4axis (supports one joystick, first joystick used),
# 4axis_2 (supports one joystick, second joystick used),
# fcs (Thrustmaster), ch (CH Flightstick).
# none disables joystick emulation.
# auto chooses emulation depending on real joystick(s).
# (Remember to reset dosbox's mapperfile if you saved it earlier)
# Possible values: auto, 2axis, 4axis, 4axis_2, fcs, ch, none.
# timed: enable timed intervals for axis. Experiment with this option, if your joystick drifts (away).
# autofire: continuously fires as long as you keep the button pressed.
# swap34: swap the 3rd and the 4th axis. can be useful for certain joysticks.
# buttonwrap: enable button wrapping at the number of emulated buttons.
joysticktype=auto
timed=true
autofire=false
swap34=false
buttonwrap=false
[serial]
# serial1: set type of device connected to com port.
# Can be disabled, dummy, modem, nullmodem, directserial.
# Additional parameters must be in the same line in the form of
# parameter:value. Parameter for all types is irq (optional).
# for directserial: realport (required), rxdelay (optional).
# (realport:COM1 realport:ttyS0).
# for modem: listenport (optional).
# for nullmodem: server, rxdelay, txdelay, telnet, usedtr,
# transparent, port, inhsocket (all optional).
# Example: serial1=modem listenport:5000
# Possible values: dummy, disabled, modem, nullmodem, directserial.
# serial2: see serial1
# Possible values: dummy, disabled, modem, nullmodem, directserial.
# serial3: see serial1
# Possible values: dummy, disabled, modem, nullmodem, directserial.
# serial4: see serial1
# Possible values: dummy, disabled, modem, nullmodem, directserial.
serial1=dummy
serial2=dummy
serial3=disabled
serial4=disabled
[dos]
# xms: Enable XMS support.
# ems: Enable EMS support.
# umb: Enable UMB support.
# keyboardlayout: Language code of the keyboard layout (or none).
xms=true
ems=true
umb=true
keyboardlayout=auto
[ipx]
# ipx: Enable ipx over UDP/IP emulation.
ipx=true
[autoexec]
echo off
mount c .
c:
type jsdos~1/readme.txt
echo on
LOGO.EXE
I’m keeping the bundles in the static/bundles
directory, which sits alongside the emulator WASM file. They’re not huge binaries but I’m still using git lfs
to manage them. Best to keep the the Git repository relatively sane.
Finally, it’s just a matter of adding some JavaScript to start DosBox, load the bundle, and mount it onto a HTML element:
Dos(document.querySelector("#element-to-mount")).run("/bundles/bundle-to-load.jsdos");
And that’s pretty much it.
After a few hours, I’ve managed to get a test version of this working. There are a few things that need working on: the app I’m trying require the Alt key, which is not readily available of Apple keyboards, so I may need to do something about that (JSDos comes with a virtual keyboard with Ctrl and Alt so it’s not a complete show-stopper)1. And I’ll need to get styling and more maintainable JavaScript written for this (I’m using StimulusJS for the JavaScript2). But I’m impressed by how well this works, given the minimal amount of effort from my part. Shoulders of giants and all that.
What would be a nice addition to the spell-check suggestions menu is a brief (3-5 words) definition of the word. I always find myself choosing the wrong suggestion, and a feature like this would help a lot.
Updating Bocce Scorecard
I didn’t get to a lot of side-project work this week, but I did have to make a large change to a project we use to track scores for our “bocce club”. So I’d though I’d say a few words about that today.
We had our bocce “grand final” a few weeks ago, and one of the matches resulted in a tie between two players. Unfortunately, the Bocce Scorecard web-app I build could not properly handle these, which meant that I had to fix it.
I’ll be honest in saying that that this was never really well fleshed out in the code, and there was actually a small bug which didn’t handle the ranking of players well. But I was pushing to keep this app as the de-facto source of truth for these matches, and there was a bit riding on this being correct (there’s a $4 trophy involved). So I had to get this fix before our next match, which was today.
Now, I been having trouble coming up with a good description of what the rules should be so I’d figured a simple example would suffice.
Imagine that there are four players: Tom, Dick, Harry, and Sally. They play several bocce matches during a season β which roughly corresponds to one calendar year β plus three “grand final” matches at the end. Each player would be awarded a number of “season points” (we informally call them “cookies”) based on how well they did in the match. The person with the most season points at the end of the last grand final match wins the season, and gets to take home the trophy.
In regular matches, the wining player is awarded one season point, while the remaining players get nothing:
Player | Score | Season Points |
---|---|---|
Tom | 11 | 1 |
Dick | 8 | 0 |
Sally | 6 | 0 |
Harry | 3 | 0 |
In grand final matches, the winning player is awarded 5 points, the one coming in second gets 2, and the one coming in third gets 1:
Player | Score | Season Points |
---|---|---|
Tom | 11 | 5 |
Dick | 8 | 2 |
Sally | 6 | 1 |
Harry | 3 | 0 |
Season points for grand final matches are distributed this way so that a single grand final match is roughly equivalent to an entire season of regular matches. This means that if someone is coming in last during the regular season (fun fact: that person’s usually me) they still has a chance to win the trophy if they do well during the grand final.
Now, let’s say that our four players are playing a grand final match, and Dick and Sally tie for second place. What should happen is that both Dick and Sally should be awarded half the season points they would get for both the second and third rank, given that they are evenly match for these two positions. In other words, they should both get 1.5 season points (1 + 2 = 3 / 2 = 1.5). Harry, who came last, still gets zero.
Player | Score | Season Points |
---|---|---|
Tom | 11 | 5 |
Dick | 7 | 1.5 |
Sally | 7 | 1.5 |
Harry | 3 | 0 |
This was the rule that I needed to change.
What I found when I started working on this is that the rule definitions themselves needed to be closer to how the players are ranked. What was previously done was that the players were sorted based on their match score, and then the rules were applied to each one by checking the win condition and awarding the points if they match it. But this didn’t fit nicely with this new approach to ties.
So instead of the conditions and awards approach, I simplified the rule definitions such that it simply defines the number of season points based on the players rank. This effectively makes it a simple map between rank and points. For normal matches the mapping would look like this:
Rank | Season Points |
---|---|
1 | 1 |
and for grand final matches, like this:
Rank | Season Points |
---|---|
1 | 5 |
2 | 2 |
3 | 1 |
Now, when a match is over, the logic that awards the season points first sorts the players based on their match score, and then groups the players into buckets such that all the players with same match score are lumped together in the same bucket. Ranks are then assigned to the players in descending score order. If two players have the same score, they will be given two ranks (e.g. Dick and Sally would have both rank two and three). Finally, season points are awarded with the rule definition and the following formula:
season_points(player) = sum[over player_ranks](rules.rank_scores[rank]) / no_of_players_in_bucket
This new logic works for ties between any number of players with any ranks.
But the introduction of division now means that the season points can be a decimal, and the database row that holds the season points is an integer type. I didn’t want to make it a floating point, so I took a page from Stripe and simply changed the representation of the season scores such that 1 season point is represented as 100 in the database. This is exposed in the rules configuration, which now looks like this:
{
"rank_scores": [
{
"points": 500,
"rank": 1
},
{
"points": 200,
"rank": 2
},
{
"points": 100,
"rank": 3
}
]
}
although all the non-admin screens properly represents the score as a decimal number.
I managed to get all finished and pushed to the server, but there was one other thing I think I’d like to get done down the line. My friends have been asking me about the outcome of previous seasons recently and I’d like to make it easier for them to view it themselves. The data exists, but it’s super hacky to get: you need to “open” a previous season so that the leader board is shown on the home page, then close it again once the info is seen. This can only be done by the admin user (i.e. me) and the screens to do it leave a lot to be desired:
What I’m thinking is adding a “Seasons” section in the web-app. Clicking “Seasons” in the nav will bring up the following screen:
The game variant will appear the top as a tab, and below them are all the current and past seasons arranged in descending chronological order. Clicking the >
will bring up the season results display:
This will show the final outcome of the season, any metadata associated with the season, and the matches of the season, along with the winner. Clicking the location will bring up the particular bocce session so that all the matches played that day can be seen.
We’ll see when I get around to building this. It’s actually been a while since I’ve last touched this project while making such a large feature.
Oh, and since it’s been a while, this usually means I needed to upgrade Buffalo, the framework this app is using. Doing this usually means that you’ll need to change your app in some way to handle the new build process. This time, it’s moving the main.go
file, previously in the project directory, into a cmd/app
directory. When you see output like this:
leonmika@Stark bocce-scorecard % buffalo build -o /tmp/app
Usage:
buffalo build [flags]
Aliases:
build, b, bill, install
Flags:
--build-flags strings Additional comma-separated build flags to feed to go build
--clean-assets will delete public/assets before calling webpack
--dry-run runs the build 'dry'
--environment string set the environment for the binary (default "development")
-e, --extract-assets extract the assets and put them in a distinct archive
-h, --help help for build
--ldflags string set any ldflags to be passed to the go build
--mod string -mod flag for go build
-o, --output string set the name of the binary
-k, --skip-assets skip running webpack and building assets
--skip-build-deps skip building dependencies
--skip-template-validation skip validating templates
-s, --static build a static binary using --ldflags '-linkmode external -extldflags "-static"'
-t, --tags string compile with specific build tags
-v, --verbose print debugging information
ERRO[0000] Error: open cmd/app/main.go: no such file or directory
You’ll need to create a cmd/app
directory and move main.go
into the cmd/app
directory.
This will get the build working again but it will break buffalo dev
as it could no longer find the main file in the project directory. To fix that, you’ll need to open up .buffalo.dev.yml
and add the following property:
build_target_path: "./cmd/app"
This will get the dev build working again.
I don’t know why the dev command honours this config, yet the build command chooses to look at a hard coded path. Wouldn’t it have been easier to express this in a single configuration file?
And let’s not leave Node out of the cold. If you’re trying to run buffalo build
and you’re getting this error:
#21 12.21 node:internal/crypto/hash:71
#21 12.21 this[kHandle] = new _Hash(algorithm, xofLen);
#21 12.21 ^
#21 12.21
#21 12.21 Error: error:0308010C:digital envelope routines::unsupported
#21 12.21 at new Hash (node:internal/crypto/hash:71:19)
#21 12.21 at Object.createHash (node:crypto:133:10)
#21 12.21 at BulkUpdateDecorator.hashFactory (/src/bocce_scorecard/node_modules/webpack/lib/util/createHash.js:145:18)
#21 12.21 at BulkUpdateDecorator.update (/src/bocce_scorecard/node_modules/webpack/lib/util/createHash.js:46:50)
#21 12.21 at RawSource.updateHash (/src/bocce_scorecard/node_modules/webpack/node_modules/webpack-sources/lib/RawSource.js:77:8)
#21 12.21 at NormalModule._initBuildHash (/src/bocce_scorecard/node_modules/webpack/lib/NormalModule.js:888:17)
#21 12.21 at handleParseResult (/src/bocce_scorecard/node_modules/webpack/lib/NormalModule.js:954:10)
#21 12.21 at /src/bocce_scorecard/node_modules/webpack/lib/NormalModule.js:1048:4
#21 12.21 at processResult (/src/bocce_scorecard/node_modules/webpack/lib/NormalModule.js:763:11)
#21 12.21 at /src/bocce_scorecard/node_modules/webpack/lib/NormalModule.js:827:5 {
#21 12.21 opensslErrorStack: [ 'error:03000086:digital envelope routines::initialization error' ],
#21 12.21 library: 'digital envelope routines',
#21 12.21 reason: 'unsupported',
#21 12.21 code: 'ERR_OSSL_EVP_UNSUPPORTED'
#21 12.21 }
You’ll need to enable the legacy OpenSSL provider using a Node option:
export NODE_OPTIONS=--openssl-legacy-provider
Yeah, building to a framework is always fun. π
So that’s it for this weeks update. I spent some time on Dynamo-Browse this week as well, but I haven’t actually finished that work and this log entry is long enough, so I might say more about that next week.
Getting some pretty strange spam emails sent to my Gmail address (which I still use). It’s the same badly formatted multi-MIME message body with different From and Subject lines. They’re trying to getβ¦ something from me? Logins, maybe? Worst phishing attempt ever!
Updates To Dynamo-Browse And CCLM
I started this week fearing that I’d have very little to write today. I actually organised some time off over the weekend where I wouldn’t be spending a lot of time on side projects. But the week started with a public holiday, which I guess acted like a bit of a time offset, so some things did get worked on.
That said, most of the work done was starting or continuing things in progress, which is not super interesting at this stage. I’ll hold off on talking about those until there’s a little more there. But there were a few things that are worth mentioning.
Dynamo-Browse
I found a bug in the query planner. It had to do with which index it chose when planning a query with only a single attribute. If a table has multiple GSIs that have that same attribute as the partition key (with different attributes for sort keys), the index the planner choose became effectively random. Because each index may have different records, running that query could give incomplete results.
I think the query planner needs to be fixed such that any ambiguity in which index to be use would result in an error. I try to avoid putting an unnecessary need for the user to know that a particular query required a particular index. But I don’t think there’s any getting around this: the user would have to specify.
But how to allow the user to specify the index to use?
The fix for the script API was reasonably simple: just allow the script author to specify the index to use in the form of an option. That’s effectively what I’ve done by adding an optional index
field to the session.query()
method. When set, the specific index would be used regardless of which index the query planner would choose.
I’m not certain how best to solve this when the user is running a query interactively. My current idea is that a menu should appear, allowing the user to select the index to use from a list. This could also include a “scan” option if no index is needed. Ideally this information will be stored alongside the query expression so that pressing R would rerun the query without throwing up the prompt again.
Another option is allowing the user to specify the index within the expression in some way. Maybe in the form of a hint, as in having the user explicitly specify the sort key in a way that does’t affect the output. This is a little hacky though β sort of like those optimisations you need to do in SQL queries to nudge the planner in a particular execution plan.
Another option is having the user specify the index specifically in the query. Maybe as an annotation:
color="blue" @index('color-item-index')
or as a suffix:
color="blue" using index('color-item-index')
Anyway, this will be an ongoing thing I’m sure.
One other thing I started working on in Dynamo-Browse is finally working on support for the between
keyword:
age between 12 and 24
This maps directly to the between
statement in DynamoDB’s query expression language, so getting scan support for this was relatively easy. I do need to make the query planner know of this though, as this operation is supported in queries if it’s used with the sort key. So this is still on a branch at the moment.
Finally, I’ve found myself using this tool a lot this last week and I desperately need something akin to what I’ve been calling a “fanout” command. This is a way to take the results of one query and use them in someway in another query β almost like sub-queries in regular SQL. What I’ve been finding myself wishing I could use this for is getting the IDs of the row from a query run over the index, and just running a query for rows with those ID over the main table. At the moment I’m left with copying the ID from the first result set, and just making a large pk in (β¦)
expression, which is far from ideal.
I’m not sure whether I’d like to do this as a command, or extend the query expression in some way. Both approaches have advantages and disadvantages. That’s probably why I haven’t made any movement on this front yet.
CCLM
I did spend the Monday working on CCLM. I coded up a small script which took some of the ideas from the blog post on puzzle design I mention last week that I could run to get some ideas. So far it’s only producing suggestions with two game elements, but it’s enough of a starting point for making puzzles:
leonmika@Stark cclm % go run ./cmd/puzzleidea
bear trap
directional walls
After running it on Monday I had a go at starting work on a new level. It became clear reasonably soon after I started that I needed a new game element. So I added one, which I’ve called “kindling”. By default it looks like a pile of wood, and is perfectively safe to walk on:
But if a fireball runs into it, it catches alight and spreads to any adjacent kindling tiles, turning them into fire tiles.
I had an idea for this for a while. I even went to the extend of producing the graphics for this element. But needing it for this puzzle finally bought me around to finishing the work. I actually manage to make most of the changes without any changes to the Go code at all: the existing tile definition configuration was almost powerful enough to represent this tile.
One other minor thing I fixed was the alignment of the info panels on the right side of the screen. Dealing with the unaligned numbers got a bit much eventually. The cursor position, marker position, and tag numbers are properly aligned now.
Anyway, that’s all for this week.
Follow-up from my earlier post about ChatGTP this morning, it turns out I probably should’ve RTFM:
Thanks to @andreab@social.lol for the tip.
Remember in GΓΆdel, Escher, Bach when they were saying that a (then) theoretical AI is less like a calculator and more akin to the human mind; and just like the human mind is likely to produce errors in the answers it gives? Wellβ¦
For reference, macOS Ventura is the current latest release.
Update: Turn’s out the reason for this is that ChatGTP’s data-set only goes back to Nov 2021. See follow-up post.
Here’s a bit of a blast from the past. I managed to get ccedit working again. This was the original level editor for workingset.net/2022/12/2… my Chips Challenge “fan game” I’ve been working on.
I’ve been designing a few levels for it recently, but since moving to a new Mac, the level editor I was using was going to be difficult to port. It’s QT application and the QT bindings were a pain to setup, and I rather not go through that again. I was using a Mac at the time I started working on it, but I wasn’t yet ready to go all in on MacOS. So to hedge my bets, I decided to go with QT as the UI toolkit.
This was 5 years ago and I’m unlikely to go back to Linux, so choosing QT was a bit of a bad decision. I think if I had my time again, I’d go with something like AppKit.
Anyway, the level editor still works but I have to log into a screen share to use it. I’d like to be able to edit levels on the machine I’m using now.
The code for the original level editor was still around but it hasn’t been touched in ages. It’s basically an SDL application β the same graphics library I’m using for the actual game itself β and the SDL v2 bindings I’m using are still maintained, so updating those were quite easy1.
One thing I did have to pull out was the Lua VM2. The editor was using old C Lua bindings. Better Lua VMs written in pure Go are now available, so I didn’t want to keep using these old bindings anymore. In fact, I didn’t want to use Lua at all. Lua was originally used for the level scripts, but I replaced this in favour of another language (which is no longer maintained π, but I’m not changing it again).
So far the editor boots up, but that’s about it. I can move the cursor around but I can’t add new tiles or load existing levels. There seems to be some weird things going on with the image name lookup. I originally thought image name were case insensitive, but after looking at the image name lookup logic in the game itself, I’m not so sure.
How much time I’d like to spend on this is still a bit of a question. It all depends whether I’d like to release the game itself in some fashion. There are still questions about whether I’m allowed to, given that the graphics are not my own. Still need to think about that.
But in any case, good to see the old editor again.
Trying out Keyboard Maestro to automate some niggly things I occasionally need to do. Got my first macro working, which converts a selected string FromCamelCase
to UPPER_SNAKE_CASE
.
So far I’m impressed. Looking forward to finding other things I can automate away with this.
On the subject of birds, I was looking at my status.lol statuses this morning. There are only a handful on there but I saw these two and it made me smile. I obviously posted them while I was looking after my sisters cockatiels last November.
Poking Around The Attic Of Old Coding Projects
I guess I’m in a bit of a reflective mood these pass few days because I spent the morning digging up an old project that was lying dormant for several years. It’s effectively a clone of Chips Challenge, the old strategy game that came with the Microsoft Entertainment Pack. I was a fan of the game when I was a kid, even though I didn’t get through all the levels, and I’ve tried multiple times to make a clone of it.
The earliest successful clone I can think of was back when I was using Delphi, which I think was my teens. It’s since been lost but I do recall having a version that work and was reasonably true to the original game as possible. It wasn’t a particularly accurate clone: I do recall some pretty significant bugs, and the code itself was pretty awful. But it was nice to be able to do things like design my own levels (I wasn’t as internet savvy back then and I didn’t go looking for level editors for the Microsoft’s release of Chips Challenge). Eventually I stopped working on it, and after a few updates to the family computer, plus a lack of backups or source control, there came a time where I lost it completely.
Years later, I made another attempt at building a clone. I was dabbling in .Net at the time and I think I was working on it as an excuse to learn C#. I think I got the basics of the game and associated level editor working but I didn’t get much further than that. Either I got bored and stopped working on it.
I started the latest clone nine years ago. I can’t remember the original motivation. I was just getting into Go at the time and I think it was both to learn how to build something non-trivial in the language, and to determine how good Go was for building games. Although this is probably just a rationalisation: I’m sure the real reason was to work on something fun on the side.
Over the first five years of its life or so, I worked on it on and off, adding new game elements (tiles, sprites, etc.) and capabilities like level scripts. One thing I am particularly proud of was building a mini-language for selecting game elements using something akin to CSS selectors. Want to select all wall tiles? Use the selector SOLD
. How about configuring a water tile to only allow gliders and the player but only if they have the flipper? Set the Immune
attribute of the water tile to GLID,PLYR:holding(flippers)
. This was particularly powerful when working on tile and sprite definitions.
I didn’t put as much effort into content however. As of today, there are only 18 or so unique levels, and about half of them are ones that I consider good. I certainly put little effort into the graphics. Many of the tile images were just taken from the original tile-set and any additional graphics were basically inspirations from that. This blatant copyright violation is probably why this project won’t see the light of day.
I’m impressed on how Go maintains its backwards capability: moving from 1.13 to 1.19 was just a matter of changing the version number in the .mod file. I haven’t updating any of the libraries, and I’m sure the only reason why it still builds is because I haven’t dared to try.
I’ll probably shouldn’t spend a lot of time on this. But it was fun to revisit this for a while.
One final thing: I might write more about projects I’ve long since abandoned or have worked on and haven’t released, mainly for posterity reasons but also because I like reflecting on them later. You never know what you’d wish you documented until you’ve lost the chance to do so.