Code in Place

Code in Place completion

Code in Place 2025 has ended. I participated as a Section Leader, tutoring a small group of beginner programmers through learning basic coding in Python. We covered fundamental programming - variables and loops and conditionals and data-structures, and used them to cajole small on-screen robots into moving around and performing tasks, and to draw some graphics with lines and filled shapes.

The course was open to all, so there were many working members of the public alongside the students. While the course is a redrafting of the famous Stanford CS106A course content, these were not generally computer science students, but instead were studying some other subject, and wanted to add a little bit of programming as an extra skill on the side.

The interesting thing about Code in Place is that it aims to investigate scaling the teaching experience, without falling prey to the usual failure modes of Massive Open-Access Online Courses (MOOCs), of poor feedback on student work and high drop-out rates. They did this by recruiting 4,000 teaching volunteers like me to act as Section Leaders. We guided the 40,000 students through the material in small classes of ~10 students, and provided 1:1 guidance and question answering in forums.

The application for section leader involved some basic competency questions, and an interesting task to record a video of me teaching some basic programming concepts. This all helped me to understand that I need to scale down my expectations of what is "self-evident", reminding me that beginners need to be taught from the beginning.

While AI was used, it was in a thoughtful, advisory capacity. For students, providing explinations of terse technical errors in their own code, and giving feedback on successfully completed tasks, to improve their solutions.

Once the classes started, I heard complaints in the teachers' forums that other classes suffered from some absenteeism, or students simply not doing the work. But my students were absolutely fabulous, reliably showing up on time, doing all the exercises, and engaging honestly and curiously in the classes. I think that students were sorted into age groups that tend to be not too dissimilar to their Section Leader, so perhaps I had an older-than-average cohort, which might partly explain how I got so lucky.

Regardless of how it happened, it was a thrillingly rewarding experience, seeing people rapidly grow their abilities, and become confident where they had originally been fearful. I'm already looking forward to next year's course!


Here

Here cover

by Richard McGuire (the 304 page graphic novel, not the original 6 page comic.)

If you dug Understanding Comics then you will probably like this. Every panel is a view of the exact same space, occupied by a modest living room in our time, but the frames span a vast time period, and are presented in a chaotic, overlapping, non-chronological way that heightens the drama of several stories that happen, at times, to pass by that location. Experimental stuff.

I hear there will be a movie. Reminds me of the movies of Alan Moore's comics, which entirely missed the point that he was exploring what could be done in the comic medium which doesn't work in others.

Blackout / All Clear

Blackout / All Clear covers

by Connie Willis, 2010

Blackout and All Clear are two novels, planned as one, but which grew out of control. They form the latest and most ambitious entry in the delightful Oxford Time Travel Series, in which Oxford University historians of 2060 perform routine observational field work in the distant past, often for weeks or months at a time.

I've positively loved several of these. The initial novella Fire Watch touchingly depicts a historian joining the watch that prevented the destruction of St Paul's Cathedral during WWII. Doomsday Book is by turns droll and tragic, telling of a young historian enduring the horrors of the black plague in the Middle Ages.

This was followed by To Say Nothing of the Dog, which follows a slightly different cast of time-travelling Oxford historians sent to an English aristocratic household in the 1890s. There, the necessity of hiding their true identities and intentions compounds the many other confusions in a comedy of manners, as a need to restore a historical object to its proper place and time, at the risk of destroying the entire continuum, is complicated by hastily improvised plans, a severe case of time-lag, overbearing matriarchs, mistaken identities, missing cats, crossed marital hopes, boating parties, and a Jeeves-like ferociously competent manservant.

Indeed, the whole thing is strikingly similar to a P. G. Wodehouse novel, and I don't say that lightly, as I think he's one of the best comedy writers ever, and this homage is entirely worthy of the comparison.

Finally, we come to Blackout, which by comparison is is a bit of a relentless downer, as historians scattered about the period of WWII sink into despair as they get trapped in the past, cut off from each other, unable to return to their future university, and slowly come to accept that not only will they have to live through the difficult and dangerous war years, and very well might never be able to return home, and further, might in fact have accidentally caused changes in the past which mean the future they came from no longer exists, and possibly even is overwritten by a catastrophic timeline in which Hitler's forces were globally victorious.

I did find it frustrating that on many different occasions the plot is driven by characters withholding information from each other, often motivated by wanting to save each other the worry of bad news. In a comedic setting like To Say Nothing of the Dog, this sort of thing only adds to the farce. But in the life-or-death precarity of Blackout, I found this to be immensely frustrating and contrived, and it happens over and over again.

Another of Willis' books that does this a lot is Passage, which isn't part of the Oxford time-travel series. I really did love it, although it absolutely has a lot of the same style of contrived mechanisms, whereby characters simply cannot bring themselves to perform this absolutely vital action because if they head in that direction, they might bump into that other character who they really dislike and who talks your ear off. But it redeemed itself with a bold and creative take on psychological events that occur during the process of brain death, which I really enjoyed a lot.

To be honest, I only stuck with Blackout because I expected that All Clear would, as the title suggests, reverse course and pay off all this dramatic build-up with uplifting, triumphant resolutions. But it was actually just much more of the same. Even the character's final escape from their predicament is interwoven with terrible sacrifice and loss.

I guess that's what a book about living through The Blitz is bound to entail. And everyone else loves it. It has a Hugo and Nebula and everything, so my own flagging enthusiasm is doubtless entirely my own failing.

Tactical Breach Wizards

Tactical Breach Wizards screenshot.

In this house, we use the metric system. Released in 2024 by Suspicious Developments.

I was toying with the idea of advancing the kiddo's videogaming curriculum into a turn-based tactics phase, maybe starting with the genius of 1994's UFO:Enemy Unknown, on which I spent endlessly fascinated evenings of my youth, or maybe one of the better of its numerous sequels and offshoots. Somehow I was distracted from that plan by multiple people enthusing about last year's Tactical Breach Wizards, and I'm so glad that I was.

It is such a lovely, synergistic blend of gameplay mechanics, setting, characters, story, plot-twists and whip-smart dialog, making substantial improvements on the traditional bombastic and yet intensely thoughtful turn-based formula.

First, it defuses the self-righteous seriousness of the genre's customary tone by replacing the gurning muscle-bound military types with a bunch of special-ops wizards. Still formidably competent, but now replete with pointy hats, hazardous runes, and bejeweled wands protruding from their assault rifle barrels. Further, while presenting a thrilling facade of enemies dispatched in a dizzying flurry of rapid-fire magic, the game explicitly disavows wanton killing. While one of our characters does sneer at the stance, your team is revealed early-on to use only nonlethal take-downs. This is soon followed up by a cut-scene which shows your team leaving a building after a mission, revealing the enemies you earlier dispatched out of eighth floor windows floating gently earthwards, each safely cocooned in a magical bubble.

Second, a fundamental mechanic bestows one of your characters with the gift of magical foresight, allowing you to see the outcome of planned actions before you actually commit to them. It's a slick narrative integration of a mechanic that serves multiple purposes. Preventing the anguish of losing a character due to dumb bad luck means the player is freed up to experiment more, trying audacious plans rather than playing it safe. Then, when it all goes wrong, you can rewind just a smidgeon, and try out nearby alternatives, until you have it all just right, bouncing generative combos back and forth between characters, unleashing staggering waves of action, discovering gleefully that a level you initially thought to be an impossible slog is actually completable in a single nimble turn. When combined with the inventive diversity of each character's specific talents, it simultaneously presents real challenges, while allowing the construction of surprising solutions that leave one feeling feeling incredibly clever and creative.

It's not often worthwhile dwelling on the characters in a videogame, but here they are the stars of the show. Distinctive, flawed and intensely likeable each in their own way, with personalities and back-stories that resonate so pleasingly with their in-game abilities. The writing is just top notch, with phenomenal dialog, giving the group as a whole a fresh, wholesome and real-talk vibe.

This is an all-time classic in my book, and has been fabulous to experience alongside the 13 year-old kiddo, as we've each run parallel games through to completion, ogling over each other's shoulders to get sneak previews of encounters we haven't seen yet.

Integer Division With Recurring Decimals

I've been doing some programming tests and puzzles while job hunting lately. One quick challenge was quite nice, reminding me a bit of Project Euler questions, and I nerd sniped myself into doing a 2nd pass at it here.

Question

Produce a Python function which takes two integers, numerator and denominator, and returns the result of their division as a decimal fraction in a string. E.g:

divide(1, 4) -> "0.25"

If the decimal places contain an infinite recurring pattern of digits, then enclose the recurring digits in parentheses. E.g:

divide(1, 3) -> "0.(3)"
divide(1, 7) -> "0.(142857)"

Wrong approaches

Evaluating the division using normal floats is going to trip you up in several ways with the limited precision.

For one, a large enough denominator might have a recurring sequence which is longer than the number of decimal places you have available (more on this later), which makes it impossible to detect recurring sequences by examining the division's decimal digits.

Worse, the inherent imprecision of floating point, e.g. if a simple division like 10/3 comes back as 3.3333333333333335, then examining the trailing digits of this looking for recurring digits is going to be problematic.

Using the decimal module does markedly improve precision and control. But infinitely repeating sequences are still going to return results like Decimal(20) / Decimal(3) -> Decimal('6.666666666666666666666666667'), which is going to trip us up.

We can sidestep all these complexities if we see that the question is asking us to perform this division ourselves, longhand. We are going back to elementary school! Wheee!

Better

Let's just do basic division first, ignoring infinite or recurring digits:

def divide(numerator: int, denominator:int) -> str:
    # Accumulate parts of our result here
    results = []
    while True:
        int_part = str(numerator // denominator)
        remainder = numerator % denominator
        numerator = remainder * 10
        results.append(int_part)

        # If there is no remainder, we are done
        if remainder == 0:
            break

        # Add a decimal point after our first integer part
        if len(results) == 1:
            results.append(".")

    return ''.join(results)

The only confusing parts of this are that int_part might contain several digits on the first iteration, but is only ever one decimal digit thereafter. Plus we have to be careful to get the ordering right for our checks to exit the loop, versus appending the decimal point to the output, to avoid weird looking outputs like divide(6, 2) -> "3.".

Trying this out:

>>> divide(1, 4)
'0.25'

It works! But we haven't yet handled infinite decimals, they result in an infinite loop:

>>> divide(1, 3) # Hangs!

Recurring digits

Because we're dividing integers, we cannot get infinitely varying decimal places. If we have an infinite number of decimal places, it must be because of a cycle of one or more recurring digits. To detect such a cycle we have to notice a couple of things.

First, simply seeing a digit in the output which we have seen before is not enough. Looking at the three assignments at the start of the above while-loop, which capture our state:

int_part = str(numerator // denominator)
remainder = numerator % denominator
numerator = remainder * 10

Here, int_part gets the value of each successive decimal digit. However if it takes on the same value as in a previous iteration, the accompanying remainder might be different, and it is the remainder which is used to generate the numerator for the next iteration, and hence generate the sequence of digits after this.

So, as we already knew from common sense, two iterations with the same int_part may go on to produce different sequences of subsequent digits. However, The value of remainder is the only thing which determines the inputs to our next iteration:

  • int_part depends on numerator and on denominator (which is constant)
  • numerator depends on remainder.

Hence, two iterations might produce different digits, but produce the same remainder, and from that point onwards, they will be in lockstep. If we find two such iterations, then we have detected an infinite recurring cycle of digits.

So, before the loop begins, initialize a dict:

# Remainders seen to date, mapped to their position in the result
remainders = {}

Then inside the loop, after everything else, use our new dict to detect if we have seen the current remainder before:

# If we have seen this remainder before, we are now in exactly the
# same state as then, so we have found a recurring digit sequence.
if remainder in remainders:
    # We have found a cycle of decimal digits! Insert parens into our results,
    # from the last seen position of this remainder, up to the current digit.
    last_pos = remainders[remainder]
    results = (
        results[:last_pos] +
        ["("] +
        results[last_pos:] +
        [")"]
    )
    break
# Remember the position at which we saw this remainder
remainders[remainder] = len(results)

Trying this out:

>>> divide(1, 3)
0.(3)
>>> divide(1, 7)
0.(142857)

OMG it works!

Defensive coding

We're putatively done, but the grumpy old dev in me is uncomfortable leaving that while True in there. By deduction, we always must eventually hit the if <condition>: break to escape from it, so ostensibly it's fine. But if I have a bug in the code or my reasoning, then it might lead to an infinite loop, in some scenario I'm not thinking of. Can we limit the number of iterations in some other, foolproof way? Turns out we can.

We've seen already that a repeated value of remainder means we can break from the loop. Also, notice that remainder, given by:

remainder = numerator % denominator

can only take values from 0 to denominator - 1. So it can have denominator possible values, and this is the maximum number of iterations we will ever need.

Hence, we can safely replace the while(True) with:

for _ in range(denominator):
    ...

Splendid! Much less anxiety-inducing

The source is on github.

SVG Trees using recursive Python functions

Inspired by a woodland hike under the first blue skies we've seen this year, I got home and showed the kiddo how to draw an SVG tree with recursive functions in Python.

At first the generated shape looked kinda lumpy and uninspiring, but it did demonstrate the principle. We were thinking of calling it a day, but I did a little bit of tweaking on parameters to control how each branch differs in length and direction from its parent. Suddenly, the generated shape really came alive, and started to look a lot more like the trees we'd seen on our hike that afternoon.

Silhouette of tree against a blue sky, drawn by a Python program

This image uses a recursion depth of 18, yielding 2^18 twigs, i.e. 250,000, which generates a 100MB SVG file. This takes about ten seconds to generate, and another ten to display in an SVG viewer. Alternatively, I can convert the SVG to a lossy webp, as displayed here, which is only 280kB and displays instantly.

Pushing the generation to greater recursion depth makes my SVG viewer and conversion tools start to stutter and barf. Presumably I could be smarter about the SVG I generate -- maybe generating the outline of the tree as points on fewer, more complex polygons, instead of a polygon for each branch segment? No matter, the artifact is the thing here, and it's done now.

Source is at https://github.com/tartley/tree-art.

The Black Parade: Level 04: Death's Dominion

So. Looking Glass's seminal 1998 PC game Thief: The Dark Project spawned an active and long-lived modding community, who created hundreds of fan-made extra levels, many of which are extremely artful and creative.

One group of particularly obsessed loons spent seven years crafting an extraordinary set of such levels, forming an entirely new single-player campaign for the game, named The Black Parade. This was released last year and I only just became aware of it. I'm four missions in, absolutely loving it, and completely lost in the catacombs beneath the pseudo-medieval city.

Hence, my lovingly hand-drawn map of mission 4, Death's Dominion:

spoilers

Map of mission 4, Death's Dominion



That Which Gave Chase

Released in 2023, played on Linux in 2024.

spoilers

Mush your dog sled across cruel Arctic wastelands, driven onwards by a brisk and intense companion, who hired you to take him back to some remote spot, where it becomes apparent he had some sort of revelation, or maybe a breakdown.

The low-res, dithered presentation conveys the harsh, blinding conditions, as you struggle to make out details through the relentless wind and ice. The days and nights of the journey blur into one another, leaving you only fragmentary, disjointed memories: sledding across the ice; arriving at crude wooden huts for the night; mounting the sled before dawn; collapsing into rough bunks; righting the sled while your companion curses you for a fool; silent moments alone.

Smash cuts amongst snowy wastes echo the discontinuities in Alan Moore's "Nemo: Heart of Ice", albeit this is a far more understated tale. The sense is of a protracted, exhausting time spent covering the distance, through punishing conditions, and it's surprisingly evocative.

The narrative leans into the disorientation, making nothing clear. Your companion becomes increasingly cryptic. He urges you onward, never pausing more than absolutely necessary. The deer behave increasingly strangely. Your companion regales you with sickening tales of the investigative mistreatment he subjected them to on his previous visit. By the time the strange mushrooms come into play it is very obvious that you are in a place to which you should never have come, very far from anywhere or anyone, with mounting dread, alone with with a madman. What happened the last time he took this route? What did he leave behind here? What awaits at your journey's end?

It's hard to know whether the difficulty of interpretation, or the non-literal aspects of your journey, are intended as the result of your character's mushroom-induced fever, or the pretensions of intrusively figurative allusions. Most likely, it seems to be both. The deliberate ambiguity runs deep.

Doesn't outstay its welcome, all done in an hour. But the memories remain.

Overhauled Manual for Epomaker Galaxy80 Tri-Mode Keyboard

Loving the new keyboard, an Epomaker Galaxy80 with Feker Marble White switches.

My requirements are pretty much the same as last time I bough a keyboard:

  • Tenkeyless layout, or TKL as it's known, i.e. without a numpad. The kiddo and I fit two side-by-side gaming stations at this desk, and the extra mouse-swiping space is precious, as is the ergonomics of putting the mouse just a few inches closer.
  • Standard ANSI layout, to match the other keyboards I commonly use.
  • Mechanical, although I'm not experienced enough to know a good one from a bad one.
  • At least two connections which are easy to switch between, for work and personal computers. This one has five, three of which are Bluetooth.
  • At least one of those connections should be reasonably low latency, i.e. <5ms, which means wired or a dedicated 2.4GHz dongle, not Bluetooth. The Galaxy80 has both. I'm a long way away from being a pro gamer, but even down here in the GamerDad leagues, I seem to be more aware of annoying latency than most people are.
  • Backlit. I don't especially care about per-key RGB, but that seems to be extremely common. Shine-through keycaps would be nice, but these seem to be increasingly rare outside of garish gamer-boi cyber-monstrosities, so not a big deal.
  • Hot-swappable switches. This is the requirement I compromised on last time I bought a keyboard, settling for the Logitech G915, which was great, but got old after switches started failing. I'm tired of desoldering them and am noping out to buy something else, a mere 16 months later.
  • Not egregiously incompatible with Linux. It would be hard to find a keyboard which doesn't actually work with Linux, but maybe some manufacturer has buried some vital configuration detail in badly written Windows-only configuration software that doesn't play nice with Wine, etc.
  • Without expensive features I don't need, like configurable activation height, or OLED screens.

The switches are described as sounding "like marbles clacking", which worried me that they might be too loud and piercing. But now it's arrived, they are actually quieter than any other mechanical switch I've had. The sound is deeper than I expected. Recognizably like marbles, but merged with the sound of pebbles, and a hint of a wooden xylophone.

I really like it! Although since pulling the trigger I've seen that Reddit doesn't like Epomaker. I'm just not going to read those posts for now.

As seems traditional with all keyboards, the manual is quite hard to read. Here's my overhauled manual, for future reference.

Update: The incantations needed to get function keys working the way you want them to on Ubuntu :eyeroll: etc. Despite the mention of 'apple' in here, this still assumes you have the keyboard switched into 'Win' mode. (via Reddit):

echo "options hid_apple fnmode=2" | sudo tee /etc/modprobe.d/hid_apple.conf
sudo update-initramfs -u

and reboot.


TIL: Constructing a PDF from .jpg image files

I have some folders of .jpg images that make up a comic. I want to convert them into a PDF to read on my tab and other devices, and import into my Calibre bookshelf.

1. Install some prerequisites

sudo apt install imagemagick pdftk

2. Do the conversion

The versatile ImageMagick has a 'convert' command that seems to handle it:

convert *.jpg output.pdf

But this has some issues:

2.1. Failure due to security policy

'convert' currently refuses to generate PDFs: 'attempt to perform an operation not allowed by the security policy'. Apply the fix described on StackOverflow. :eyeroll:

2.2. Failure due to cache space

You might not need this fix if you generate smaller documents, or generate chapter-by-chapter as described below, but here it is in case.

Don't close that editor! In the same policy.xml you were just editing are resource size declarations for memory and disk. If 'convert' barfs with an error about running out of cache space, then bump up the disk resource size. I set mine to 8GB. StackOverflow again for details. :eyeroll: again.

3. Include a table of contents

I want to add bookmarks to the generated PDF marking each chapter.

Put the .jpgs into subdirectories by chapter, eg:

src/
|--chapter01/
|  |--0001.jpg
|  |--0002.jpg
|  |  ...
|--chapter02/
|  |--0001.jpg
|  |--0002.jpg
|  |  ...
|
...

Pad the chapter numbers with preceding zeros so that they sort into the correct order. I added an artificial 'chapter00' containing the front cover, separate from individual chapters.

Now we need to generate individual PDFs for each chapter. We can then use 'pdftk' to count the number of pages in each chapter, and use those counts to place bookmarks on the correct pages when pfdtk combines the chapters into one final output PDF.

I ended up regenerating each chapter a bunch while I tweaked the content, such as deleting adverts from the images. So I put these commands into a Makefile:

help: ## Show this help.
    @grep -E '^[^_][a-zA-Z_\/\.%-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-12s\033[0m %s\n", $$1, $$2}'
.PHONY: help

chapter_dirs=$(wildcard src/*)
chapters=$(chapter_dirs:src/%=%)
chapter_pdfs=$(chapters:%=%.pdf)
bookmarks=bookmarks.txt
output=output.pdf

clean: ## Delete all generated PDFs
    rm -f $(chapter_pdfs) $(output)
.PHONY: clean

chapter%.pdf: src/chapter%/*.jpg ## Each individual chapter, use 2 digits
    convert src/chapter$*/*.jpg $@

$(bookmarks): $(chapter_pdfs)
    ./make-bookmarks >$(bookmarks)

$(output): $(chapter_pdfs) $(bookmarks)
    pdftk $(chapter_pdfs) cat output - | \
    pdftk - update_info "$(bookmarks)" output "$(output)"

all: $(output) ## Build final output PDF
.PHONY: all

Where 'make-bookmarks' is a bash script that generates the intermediate 'bookmarks.txt' file:

#!/usr/bin/env bash

set -e # exit on error
set -u # treat unset vars as errors
# set -x # debugging output
set -o pipefail

# Generate a bookmarks file for all the matching PDF files

fmt="BookmarkBegin
BookmarkTitle: %s
BookmarkLevel: 1
BookmarkPageNumber: %d
"

declare -a files=(chapter*.pdf)
page=1
for file in "${files[@]}"; do
    title="${file%.*}"
    printf "$fmt" "$title" "$page"
    num_pages="$(pdftk "$file" dump_data | grep NumberOfPages | awk '{print $2}')"
    page=$((page + num_pages))
done

Now make all will produce the final output.pdf. You might want to open up the generated bookmarks.txt and edit the placeholder "chapter01" names. Then run make all again to regenerate the final output PDF with your fixed chapter names.

Rorschach II meets Adrian