← All blog posts

The two applets I was never going to rebuild

September 16, 2026 · AI

On the left, a browser window from 1999 whose applet area is an empty grey box reading "Java Applet: plug-in not available". An arrow labelled rebuilt, 2026 leads to two windows: a desktop jar showing the eight-puzzle board, and a browser page showing a small Sokoban maze. Below, two panels: the applet's Solve button never produced a solution, for three independent reasons; and the level converter silently deleted every box that started on a goal, damaging 36 of 59 levels.
Two applets from 1999, and the four programs they became. Underneath, what searching the whole state space turned up: a Solve button that had never once solved anything, and a level converter that had been quietly deleting boxes since 2003.

In October 1999, in the Artificial Intelligence Laboratory of the University of Piraeus, Theodoros Theodorou and I wrote an 8-puzzle for a course assignment, supervised by Themis Panayotopoulos. It was a Java applet, because in 1999, if a thing had to run inside a browser and do more than blink, it was a Java applet. Around the same time I ported Sokoban to Java on my own, and signed the source the way we signed things back then; Sokoban Ver. 1.00 - Java Port By ReneGade/TiS. (ReneGade/TiS was my handle. It is on the about page now, so it is not much of a secret any more.)

Both have been sitting on my software page ever since, as a zip and an <applet> tag. No browser has executed one of those since roughly 2015. When I swept this site's archive two weeks ago I deleted 67 .class files that nothing on earth can run. That is what a Java applet is today; a folder of dead bytecode with a screenshot next to it, kept for sentimental reasons.

Last Tuesday night I rebuilt both of them. By Wednesday afternoon each one was a desktop application on JDK 17 and a JavaScript version that plays in a browser tab. Four programs, about thirty-five hours of wall clock, and I did not write the code.

What I am actually admitting

Let me put this before the technical part, because it is the honest part of the post. I could have done this at any time in the last twenty years. There is nothing in these four programs that is beyond me; a sliding puzzle, an A* search, a canvas, a level parser. This is second-year coursework and I have been paid to write harder things every year since.

But I was never going to do it. Not once. The work is not hard, it is long. Reading 710 lines of 1999 AWT to recover what the rules actually were. Pulling 75 JPEGs out of a build that fetched them by URL. Writing the tests that prove the search is right rather than plausible. That is two or three weekends, and my weekends have a family in them. Every time I opened that directory, I closed it again.

So what changed is not capability, it is price. The models made this cheap enough to be worth doing, which is a smaller claim than the one the vendors make and a more interesting one. They did not give me a skill I lacked. They gave back an afternoon I was never going to spend. (I remain sceptical about what these tools deliver in day-to-day engineering, and I have written so more than once. A small, closed, twenty-seven-year-old problem with a working game at the end of it is a very different shape of thing.)

The same recipe, twice

Both ports ran the same four steps, in the same order; keep the original, treat the artwork as data, rewrite the rules instead of translating them, and find an oracle. If you ever do this to something of your own, that order is most of the value.

Keep the original. The 1999 sources live in each repository under legacy/, compiling nowhere and shipped by nothing. They are the specification, and they are the only copy. For the 8-puzzle there are two of them, and they are not the same program: the development directory, and the build that actually ran on the website, recovered from this site's own repository. The deployed one has an about screen whose source exists nowhere. The development one has a fourth skin, White Star, that never went online. It is a photograph cut into nine, the only true picture puzzle of the four, and as of yesterday it is finally published.

Artwork is data. Every tile face, logo and banner is the 1999 bitmap, byte for byte. The browser versions do not keep their own copy of anything. A generator script copies the images out of the desktop game's resources and records a SHA-256 per file. So a well-meant optimisation pass over the artwork shows up as a stale build, rather than as a board that quietly looks different in one place. The single exception is an offline upscale of Sokoban's 20×20 tiles with EPX, which bends a pixel only where two neighbours agree and therefore keeps the pixel lettering sharp.

Rewrite, do not translate. The temptation with an old applet is to change Applet to JFrame and call it a port. Both rebuilds refused that. The 8-puzzle board is now an immutable value; a move returns a new board, and the solver can hold 181,440 of them in a map without any of them changing underneath it. The search runs on a SwingWorker, in place of a raw Thread that slept 1.5 seconds between steps and was cancelled with Thread.stop. The applet's 710 lines became 1,684 plus 356 of tests. Sokoban's 1,236 became 1,376 plus 264.

Find an oracle. This is the step that pays, and it is the one everybody skips. How do you know a rewritten game is the same game? The 8-puzzle has 181,440 reachable arrangements, which means you can breadth-first search the entire state space once and know the true distance of every board that exists. The test suite does exactly that and then checks the A* answer against it for 300 random boards plus both of the 31-move worst cases. Not "the solution looks short"; provably shortest. Sokoban has no such luxury; its levels run from 6 boxes to 152. So the 65 small ones are proved winnable by search over the real rules, and every imported level is rendered back out of the parsed model and compared to the source text, character for character.

And the oracle is what found the two bugs, both of them older than some of my colleagues.

The solver that never solved anything

The 8-puzzle applet has a Solve Problem button, a solutionThread and a solClass search tree. It never once put a solution on the screen, for three independent reasons.

The states were aliased. getNextState does int[] _tmp = state;, a reference to the parent's array rather than a copy, and then writes into it. Every node in the tree shares one array, so a move corrupts the position it came from. Given a board one slide away from solved, the search visits 43 positions, none of them a legal successor of the last, and returns having never seen the goal.

Nothing was wired to the display. The end of a successful search walks up the parent chain and calls System.out.println on each state. On a web page in 1999 that went to the Java console, which nobody had open.

And the deployed build could not even start the search. The copy that actually ran on the website ships eight class files, and solClass.class is not among them; the first thing the solver thread does is construct one.

There is a fourth, if you want it. The move generator guards the top and bottom rows, but tests the left and right ones with || chains that are true on every square, so a search starting with the empty square in the top-left corner throws ArrayIndexOutOfBoundsException: Index -1 on its first expansion. Three reasons were already enough.

The solution list on screen was only ever filled by hand, by clicking tiles in "make puzzle" mode. Twenty-seven years later I know why the demo worked.

While we are here, two more. Generate Random Game swapped eighteen random pairs of squares, which lands on an unsolvable board exactly half the time (50.00% over 200,000 runs of its own shuffle). And nothing anywhere checked whether the player had won. We got a grade for this :)

The converter that ate the boxes

The Sokoban bug is worse, and for once it is not mine.

The applet shipped 59 levels from LOMA (Levels Of Multi Authors, assembled by Aymeric du Peloux). They had been converted into the applet's own map format by SoKonvert, a utility George Oikonomou wrote for it, still downloadable at the bottom of that page. Fed to the rewritten parser, all 59 read cleanly and the box count matched the goal count in every one of them. So far so good. Then breadth-first search over the real game rules reported 25 of them unsolvable, and most of the rest winnable in a handful of moves (LOMA09-02 in a single push).

The converter drops *, the character for a box that starts already standing on a goal, and writes plain floor instead. That removes a box and a goal at the same time, so the counts stay balanced and nothing looks wrong. 36 of the 59 were damaged this way. LOMA03-01 is a three-box puzzle; the copy this site served for twenty years had one box and no reachable goal at all. The second collection in the same zip, Sasquatch III by David W Skinner, lost 358 boxes-on-goals across 38 of its 49 files.

So both rebuilds read the levels from levtext.txt, the collection's own text, which is where SoKonvert read them from too. That yields 60 levels instead of 59 and 50 instead of 49; two levels exist in the source that the converter never emitted, so the site never had them. With the five original maps, 115. What guards it from here is one number. 415 boxes start on a goal across the whole catalogue; a test asserts that total, and the browser generator refuses to write a file in which it has changed.

In the browser, again

The browser versions are one file of plain ES2020 each, 745 lines for the 8-puzzle and 461 for Sokoban. No build step, no dependencies, no framework, a canvas redrawn from the model on every change. The A* runs on the main thread because it can afford to; the worst board takes about 5,000 arrangements and 20 milliseconds, and the page still has time to put "Searching…" on screen first.

This is the part that pleases me most. The 1999 applet needed a plugin, an <applet> tag, a codebase, a MediaTracker and eighteen HTTP requests every time you changed skin. Its 2026 replacement is a static file with nothing behind it, and it will still open in fifteen years, which is more than the original managed.

Epilogue

So what did I do, exactly? I asked. I read the diffs. I played both games until I was satisfied, and I decided the arguments: one Sokoban page instead of three, the tiles upscaled eight times rather than four, the White Star skin shipped rather than left in the archive. Roughly the work of a reviewer, and the review is the part I could not hand over. A model can tell me the search is optimal. It cannot tell me that a photograph cut into nine pieces in 1999 is worth putting back on the internet.

And the bill? There are two more repositories with my name on them now. In three years something will break in one of them, and the code I open will be code I did not write. That is the real cost of this kind of afternoon and it is not discussed much. Here I am not worried (a sliding puzzle is not going anywhere) but I would think a good deal harder before doing the same thing to software with users in it.

Both games are playable: the 8-puzzle and Sokoban, in the browser or as a jar, with the 1999 sources kept beside them in bkarak/8puzzle and bkarak/shokoban.

There are two more Java/applet tags in that list, a binary thresholding filter and a shape drawing toy, from the same years and the same lab. We'll see.