V5 api slim#26
Merged
Merged
Conversation
First pass of the v5 API slimming, following V5-API-REVIEW.md. Everything removed here either had no Scratch block equivalent and no demonstrated use, or was a second way to do something that already had a first way. - the setWhenX family: 17 setters and 15 nested When*Handler interfaces. Every event already has an overridable whenX(), the form that matches Scratch. - setRun; override run() instead. - setOnEdgeBounce; call ifOnEdgeBounce() inside run(), like the block does. - twelve typed collection queries on Stage (countSpritesOf, findPensOf, removeAllTexts, ...) in favour of the generic find(Class)/count(Class). - getWindow on both classes; use Window.getInstance(). - removeSound on both classes, Stage.removeBackdrop. Sprite.keyEvent/mouseEvent become package-private. Stage.goToFrontLayer(Sprite) and its three siblings, and the three removeAllX() helpers, become non-public: they are the implementations behind the Sprite methods and removeAll(), not duplicates of them. Four verdicts from the review were corrected while applying them, and are marked inline in V5-API-REVIEW.md: Stage.display is user-facing and stays; Stage.draw/pre/keyEvent/mouseEvent cannot be dropped because internal.Applet calls them across a package boundary; and Sprite's width/height methods are the sizing API for nine-slice UI sprites, so they move with nine-slice rather than being dropped. 293 -> 252 public methods. 151 tests pass; the cat, pipes, stressTest, timedDot, shader and tiled demos were updated, and the cat demo was re-run headless to confirm it still bounces off the edges.
Second pass of the API slimming, 36 methods off Sprite and Stage.
Clock replaces the nine current-date methods that existed on both classes,
18 methods for 9 values. They are real Scratch blocks ("current [year]",
"days since 2000"), so Clock sits in the core package rather than in an
extension, and is covered by import org.openpatch.scratch.*.
Shaders replaces the nine shader methods that also existed on both classes.
Each class keeps a single getShaders() accessor; add/switchTo/next/reset and
the current-shader queries live on the returned object.
That accessor shape is worth noting: several remaining MOVE entries in
V5-API-REVIEW.md are themselves accessors (getPen, getText, getCamera,
getHitbox, getTimer), and they should stay as the door to their extension
rather than being moved away. The operations behind them are what moves.
The clock and shader demos and the clock reference examples were rewritten.
SpriteGetCurrentTime and StageGetCurrentTime collapsed into one
ClockGetCurrentTime, since they now differ only in whether a sprite says the
values or the stage displays them.
293 -> 218 public methods. 151 tests pass; the shader demo was re-run headless
to confirm both the sprite and stage shaders still apply.
Third pass of the API slimming, 10 more methods. getSorting() replaces setSorter/enableYSort/disableSort/isSortEnabled, and getPixels() now hands back a Pixels object covering the main, background and foreground layers instead of just an int[] of the main one. Neither group was used anywhere in the demos or reference examples. Stage.setTextureSampling is dropped rather than moved: it duplicated the existing Window.TEXTURE_SAMPLING_MODE static. addTimer and removeTimer are dropped by fixing the design instead of relocating it. getTimer(name) now creates the timer on first use, so a named timer no longer has to be declared up front - the same add-then-use ceremony that sounds had. AnimatedSprite.playAnimation had been carrying a null-check and an addTimer call purely to work around it; both are gone. Two review verdicts corrected while applying, noted in V5-API-REVIEW.md: setHitbox stays (seven real uses; it is the escape hatch for precise collision shapes, and its problem is four overloads, which is step 3), and setCursor stays (per-stage state, and a menu stage wanting its own cursor is reasonable). 293 -> 208 public methods. 151 tests pass; the timer demo was re-run headless to confirm several named timers still fire independently without addTimer.
Fourth pass of the API slimming, 9 methods. Buttons and bars want what an ordinary sprite does not: to be drawn on top and ignore the camera, and to be sized in pixels rather than percent so nine-slice scaling can stretch one costume to any size. extensions/ui/UISprite carries that. setWidth, setHeight, setNineSlice and disableNineSlice become protected on Sprite and are widened to public on UISprite, which is the Java way to move a method off a public surface while leaving the implementation next to the state it touches. changeWidth/changeHeight live only on UISprite. isUI(boolean)/isUI() become protected setUI(boolean)/isUI(). Stage.goToUILayer is dropped: nothing used it, and it only removed the sprite from the stage's list, which reads like a bug rather than a feature. getWidth/getHeight stay on Sprite, correcting the review: the y-sorting added in the previous commit uses getHeight(), so they are needed generally and only the setters are nine-slice-specific. The ui demo's Button and Bar now extend UISprite. The sensing and tiled demos promoted an ordinary sprite class to the interface layer at runtime, which a subclass cannot express, so each gained a three-line subclass (UIHero, UIItem) calling the protected setUI(true). That keeps the capability without putting the flag back on Sprite's public surface. 293 -> 199 public methods. 151 tests pass; the ui demo was re-run headless to confirm the nine-slice panel and bars still stretch with their corners intact.
The pen/stamp group in V5-API-REVIEW.md cannot be moved. Pen, in extensions/pen, calls sprite.stampToForeground() and stage.eraseForeground(); TiledMap, in extensions/tiled, calls stage.addStampsToForeground(). Those are different packages, so Java forces the methods to stay public - the same wall that Stage.draw and pre run into with internal.Applet. Rather than force a bad refactor, the six plumbing methods are tagged @ignore-in-docs so they leave the reference site while staying callable. Sprite.stamp() and Stage.eraseAll() stay visible; both are genuine Scratch pen blocks. This splits the metric in two: 199 public methods, 14 of them hidden, so a documented surface of 185. Getting the plumbing truly off the public API needs an architecture decision - merging extensions/pen into the core package, a JPMS descriptor that does not export it, or an internal bridge type - which is worth settling before step 4 starts moving packages.
Stage.draw, pre, keyEvent and mouseEvent were public for one reason: Applet lives in another package and has to call them. They are now private. Stage hands the render loop a private object implementing internal.StageHooks. Stage deliberately does not implement the interface itself - a class implementing a public interface must declare those methods public, which is the thing being avoided. Nothing new became public in exchange. Every path that hands Applet a stage already starts inside the core package: Stage's own constructor, and Window.setStage / transitionToStage / addStage. So Applet keeps a weak map from stage to hooks, filled by the stage when it is created, and dispatches through that. Worth recording that JPMS would not have solved this. It controls which packages are exported, not member visibility on exported types, and Sprite and Stage have to be exported. The module-info.java_bak in the repo notes the library is not shipped as a module anyway, since Processing is not one. 293 -> 195 public methods; 10 methods remain public-but-hidden, all of them reachable the same way once Pen moves into the core package and TiledMap gets a documented stamp method. 151 tests pass. Verified headless that a stage still renders on its own and that Window.transitionToStage still fades from one stage to the next, since a missing hook would show up exactly there.
addStampsToBackground and addStampsToForeground were public, two overloads
each, for one reason: TiledMap stamps map layers and lives in another package.
They are now package-private, together with addStampsToUI, and a single
documented method took their place:
stage.stamp(stamps, Layer.BACKGROUND);
Layer is a new core enum - BACKGROUND, FOREGROUND, UI - so the layer is
something you say rather than something encoded in a method name. That turns
four pieces of plumbing into one piece of real API, which is the better trade
before even counting methods.
192 public methods now, 6 of them hidden. All six remaining have a single
cause: Pen, in extensions/pen, calls Sprite.stampTo* and Stage.erase*. Moving
Pen into the core package clears all six at once and is where it belongs
anyway, so that goes with step 4's package work.
151 tests pass; the tiled demo was re-run headless to confirm its map layers
still stamp.
Step 3 of the API slimming, and deliberately small: of 37 surplus overloads only 8 go, the ones that cause confusion rather than the ones that merely add to the count. broadcast(Object) and whenIReceive(Object) leave both classes. Sitting beside the String versions they were a silent-failure trap: broadcasting anything that was not a String dispatched only to the Object handler, so a sprite overriding whenIReceive(String) heard nothing at all. Neither was used anywhere, and Scratch messages are names. whenAddedToStage(Stage) and whenRemovedFromStage(Stage) leave Sprite. One event had two override points - both were invoked, so either worked, which is exactly the sort of thing that is hard to explain - and the argument is what getStage() already returns. setHitbox(Hitbox) and setHitbox(double[], double[]) leave Sprite, keeping setHitbox(Shape) and setHitbox(double... points). The single Hitbox call site was wrapping an Ellipse that the Shape form accepts directly. The remaining 29 surplus overloads stay on purpose: say(text)/say(text, millis) mirrors Scratch's two blocks, add(Sprite)/add(Text)/add(Pen) is what Java requires for distinct types, and the Vector2 forms of move, setPosition and setDirection are used across the demos. 293 -> 184 public methods across 155 names. 151 tests pass; the sensing demo was re-run headless with debug drawing on to confirm the rewritten elliptical hitbox is identical and still registers hits.
Step 4. Nine extension packages move into the core package - pen, text, timer, animation, ui, math, shape, color, hitbox - so that one import covers what a school course actually touches. Utils went to internal instead, since only the library uses it. Seven packages stay opt-in: camera, fs, pixels, recorder, shader, sorting, tiled. Across the examples the imports are now 177 org.openpatch.scratch, 27 recorder (only in the doc-generating reference examples), 2 tiled and 1 fs. The point of doing this as one pass was the visibility it buys. With Pen beside Sprite and Stage, the six stampTo*/erase* methods that were public only so Pen could reach them from another package are package-private. That takes the hidden-from-docs count to zero: 179 public methods, all of them documented, where before v5 there were 293 with a growing pile of plumbing among them. stampToUI turned out to be a real user call in the tiled demo rather than plumbing, so Sprite.stamp(Layer) replaces the three stampTo* methods, matching Stage.stamp(stamps, Layer). Two references stay fully qualified because their collisions are real: tiled/MapObject uses org.openpatch.scratch.Polygon next to its own Polygon, and internal/AnimatedGifEncoder uses org.openpatch.scratch.Color next to java.awt.Color. Six test classes moved to follow their packages; surefire discovers tests by directory, so leaving them behind silently ran none of them. 151 tests pass. The tiled demo was re-run headless afterwards, since it exercises TiledMap, Text, stamping and the new Layer together.
Step 5. The examples were not missing - 126 of them sit under src/examples/java/reference/ - they were simply not connected. The doclet only emits an example when the method carries @example.preview, and exactly one method did, so the reference site showed a single worked example for the whole library while 115 finished examples went unused beside it. Wiring them by name, SpriteSwitchCostume.java to Sprite.switchCostume, hooked up 107. Example coverage goes from 0.1% to 22%. Two were removed instead of wired: SpriteAddTimer, whose method no longer exists, and SpriteGetCurrentTime, which after the Clock work was a duplicate of ClockGetCurrentTime differing only in whether a sprite says the values or the stage displays them. Checking the built pages turned up a separate problem: the doclet only writes reference pages, it never deletes them, so docs/book/reference had been accumulating entries for methods removed in steps 1 to 4 - addTimer, getCurrentDay, addShader, setWhenKeyPressed and 425 others. The rendered nav listed all of them. A clean step before the doclet runs takes the reference from 914 entries to 485. Two stale @see references to the pre-flattening package names were also fixed; they were breaking the javadoc jar. 151 tests pass, mvn package succeeds, and the generated site was checked to confirm a wired page renders its gif.
91 methods gained a @scratchblock annotation, taking the reference from 23 annotated pages to 113. A page now renders the block a learner already knows directly beneath the Java that replaces it - .ifOnEdgeBounce() beside "if on edge, bounce" - which is the bridge the whole library exists to build. The mapping was made by walking Scratch's palette rather than the Java API, so only methods with a real block equivalent got one. Vector2.dot, Color.getHSB, Operators.lerp and the rest of the maths helpers are deliberately left bare; annotating them would have inflated the number while teaching nothing. Also tidied 13 single-line javadocs that the annotation pass had left in the form "/** text *", and two that still carried residue from removing @ignore-in-docs when the erase methods became package-private. Reference coverage against the pre-v5 state: 485 entries rather than 914 with 429 stale, 113 with a scratch block rather than 23, 108 with a worked example rather than 1, and 62 with both. 151 tests pass, mvn package succeeds, and the rendered page was checked to confirm the block draws as a real Scratch block rather than as text.
Getting Started contained no Java at all - install BlueJ, download a zip,
right-click. It now walks through a seven-line program that puts a rabbit on
screen with nothing to download, then makes it walk with the arrow keys, with
the equivalent Scratch script rendered beside the Java. Both programs were
compiled and run headless, and the two gifs on the page are recordings of them.
Two pages had become actively wrong. "Costumes, Backdrops and Sounds" opened
with "Scratch for Java does not come with any of that", and "Differences to
Scratch" said there is no sprite or sound library; both stopped being true when
838 sprites and 266 sounds moved into the jar. They now point at the Sprites and
Sounds pages and explain when to use a path instead of a name.
Every Java block in the beginner pages was extracted and compiled against the
current API. That found two samples broken since KeyCode became an enum, well
before v5: the Bouncing Hedgehog tutorial declared whenKeyPressed(int keyCode)
and compared against KeyCode.VK_LEFT, neither of which exists. The Multiple
Approach Design page used setRun and setOnEdgeBounce, both removed in v5; its
imperative example now drives every sprite from the stage's run() instead.
build.sh substituted {{VERSION}} in place and never put it back, so the
placeholders in download.md and index.md had already been overwritten with
4.28.1 and every later release would have shipped stale download links. The
substitution is now undone after the build, and setup.md joins the pages that
get it rather than hard-coding 4.23.0.
The setWhenX family left Sprite and Stage in the first v5 pass, but Window was not part of that sweep and kept the same pair: an overridable whenExits() beside setWhenExits(handler), with a nested WhenExitsHandler interface. Only whenExits() was ever used - the PenUp reference example overrides it - so the setter, the interface and the handler field are gone and whenExits() is now a plain override point. build.sh ran only `mvn compile`, so the reference pages were whatever the last `mvn package` had produced. That is how setWhenExits.md.json outlived the method by a whole commit. It now runs `mvn -DskipTests prepare-package`, which regenerates the reference and the built-in asset pages together. The imperative section of Multiple Approach Design was also reworded: it described using "only the built-in classes Stage and Sprite", which stopped being exact once the example gained `extends Stage`. 151 tests pass, and a full ./build.sh leaves no set-handler page anywhere in the reference.
Three of the six downloadable project archives no longer compiled: cat used setOnEdgeBounce, and bouncy-hedgehog-100 and Halloween still imported the pre-flattening packages and used KeyCode.VK_* names that stopped existing when KeyCode became an enum. bouncy-hedgehog-100 is the finished version of the Bouncing Hedgehog tutorial, so a student working through it and downloading the solution to compare got a project that would not build. All six compile now, and the cat project was run headless to confirm it still bounces after moving from setOnEdgeBounce(true) to ifOnEdgeBounce(). A sprite with no costume used to draw nothing, which is indistinguishable from a broken program. Sprite.draw now puts a marked box with a question mark where the sprite is and explains once on the console what to add. This was the oldest item on the beginner-friendliness list. Asset failures throw ScratchException rather than calling System.exit. Ending the process took BlueJ's virtual machine down with it, so one mistyped costume name closed everything the student had open. The detailed message is unchanged; only what happens afterwards differs. Five call sites across images, sounds, fonts and shaders. Removing the exit also let the compiler prove one branch in Shader unreachable, so a dead `return null` went with it. The two AssetErrorReporter methods were renamed from reportAndExit and reportUnknownBuiltinAndExit, since neither exits any more. 151 tests pass, ./build.sh succeeds, and both new behaviours were checked by running them: the question-mark marker renders, and a typo'd costume name now prints "Did you mean: bunny1_stand" and leaves the process running.
Stage had seven constructors and Window eight, four of each differing only by a
leading boolean fullScreen. That combinatorial set is the first thing BlueJ
shows in its "new Stage()" dialog, and telling Stage(boolean, int, int) from
Stage(int, int, String) is not a reasonable thing to ask of a beginner.
Full screen becomes a setting instead, beside the ones that already were
settings:
Window.FULL_SCREEN = true;
new MyStage();
The tiled demo already set Window.TEXTURE_SAMPLING_MODE and several Text
defaults in exactly that position, so the idiom is not new.
Stage keeps Stage(), Stage(width, height) and Stage(width, height, assets);
Window keeps those three plus Window(assets). Consolidating also removed a
duplicated copy of the "only one Window" check that had been living in two
constructors at once.
Verified by recording the rainbowVine demo both ways: the surface is 480x360
normally and the full screen size with the flag set. Worth noting that
Stage.getWidth() reports the requested size rather than the actual surface in
full screen, which is pre-existing and not addressed here.
151 tests pass.
Scratch's "ask () and wait" and "(answer)" had no counterpart, which ruled out a whole category of first projects - quizzes, guessing games, anything that greets you by name. It was the largest genuine gap left after walking the block palette. A box appears at the bottom of the stage, where Scratch puts it too, and collects characters until Enter. Backspace deletes. ask, getAnswer and isAsking are on both Sprite and Stage, as in Scratch. It cannot pause the script the way Scratch does, because this library has no per-sprite wait, so run() keeps being called while the question is up and the program checks isAsking() or waits for getAnswer() to change. The javadoc says so and shows the polling shape. While a question is waiting, key events go to the answer instead of to the sprites, so nothing runs around the stage while someone is typing their name. Verified by driving it headless: injecting "Mike" then Enter leaves isAsking() false and getAnswer() equal to "Mike", and the recorded frames show the box rendering the question above the answer as it is typed.
Three more of the gaps found by walking Scratch's palette. glide(seconds, x, y) slides a sprite instead of jumping it, with isGliding() to ask whether it has arrived. It is stepped once per frame from the stage, so like ask it does not hold the sprite up. setVolume, changeVolume and getVolume appear on Sprite and Stage. Sound had volume all along and nothing exposed it. The values are percentages as in Scratch and are clamped to 0..100. Stage.waitUntil(condition) waits for something to become true, built on the existing wait(millis), so it holds up only the calling code while sprites keep running. Three demos, none of which need an asset file: quiz asks three questions and reacts to the answers, glide has an alien that glides to the mouse and a coin patrolling two corners, and volume plays a sound at a volume the arrow keys change. isTouchingColor was written and then dropped. Reading a colour off the stage means pulling the whole screen back from the graphics card, which turns out to be possible only while the stage is drawing and costs a full-screen copy every frame - around 1.4 MB per frame for three layers at 400x300. That is too expensive for the hardware this library targets, and the first version also quietly returned false because the pen draws to the background layer while it sampled the main one. The Pixels class keeps the same limitation and now says so in its documentation. 151 tests pass. The quiz was played through headless - three questions answered, the score reacting - and glide was verified by tracking the coin across four direction changes.
Window.FULL_SCREEN and Window.TEXTURE_SAMPLING_MODE were public fields to be assigned before a stage was created. Assigning one afterwards did nothing: no error, no warning, just a windowed program and no clue why. I introduced FULL_SCREEN two commits ago while cutting the constructor count, so this fixes a silent failure of my own making - the same class of failure the rest of this work has been removing. They are static methods now, useFullScreen() and useTextureSampling(...), which show up in autocomplete on Window, read like Math.random() rather than needing the idea of a static field, and print an explanation when called too late instead of quietly doing nothing. TextureSampling is an enum with POINT, LINEAR, BILINEAR and TRILINEAR rather than the bare numbers 2 to 5, so the pixel-art setting reads as POINT instead of TEXTURE_SAMPLING_MODE = 2. Out-of-range values are now impossible, so the runtime check that used to catch them is gone as well. Verified both ways: called in time, the surface is the full screen and the sampling reports POINT; called after the stage exists, both print a warning naming the exact line to write and the program carries on windowed. Text.DEFAULT_FONT and its neighbours are left alone for now. The same criticism applies, but they are settings a first course never touches. 151 tests pass.
Text.DEFAULT_FONT, DEFAULT_FONT_SIZE, FONT_SIZES and SMOOTHING had the same weakness as the Window globals: the font is loaded while the window starts up, so assigning one afterwards did nothing useful and said nothing about it. They become useFont(path), useFont(path, size), useFontSizes(...) and useSmoothing(boolean), each printing an explanation when called after the window exists, matching what useFullScreen and useTextureSampling now do. The readers are getDefaultFont() and getDefaultFontSize() rather than getFont() and getFontSize(), because Text already has an instance getFont() returning the font that one piece of text uses. Java will not allow both, and the clash is a useful reminder that they are different ideas. Font gained a reset() so a changed setting clears the cached fonts rather than being outlived by them. Verified in both orders: chosen before the stage, "Hello Scratch" renders in the pixel font at the chosen size; chosen afterwards, the warning names the exact line to write. An earlier version of this claimed the setting could be changed at any time - it cannot, reliably, and the documentation now says so instead. The tiled, donutIO and shakespeare demos were updated. 151 tests pass.
Three things. TextAlign held four mutable public ints, so TextAlign.CENTER = 5 compiled and would have broken alignment across a whole program, and setAlign(int) accepted any number. It is an enum now, matching what TextureSampling became. The demos needed no edits: they already wrote TextAlign.LEFT, which now type-checks instead of resolving to an int. Text also gained getAlign(). The question-mark marker for costume-less sprites was firing on sprites that have no costume deliberately. The tiled demo builds its walls, water and warp triggers as sprites with a hitbox and nothing to draw, and every one of them got a red marker painted on it. A sprite given a collision shape but no costume is a normal thing to build, so the marker and the warning now apply only to sprites with neither. Re-running the tiled demo shows a clean map and zero warnings, where before it was covered in markers. The quiz, glide and volume demos arrived with the ask, glide and volume work but never got documentation pages, so they existed only in the repository. Each has one now, with recordings for the two worth watching. 151 tests pass. Alignment was checked by rendering left-aligned and centred text side by side.
The path went straight from "your first program" to a finished game with no steps in between. Catch the Coins and Guess the Number fill that gap, and neither needs a downloaded file. Catch the Coins is the first project that has to remember something: a score kept on the stage, a for loop that makes four coins rather than copying a sprite four times, a method of your own standing in for a custom block, and Random.randomInt for the pick-random block. Guess the Number is the first project that asks the player something. It also carries the one place ask() differs from Scratch: it does not pause, so the program checks isAsking() itself. Writing it turned up the bug that difference causes. Once the number is guessed no new question is asked, so isAsking() stays false and run() counts the same answer again every frame - the first recording proudly announced the number had been found "in 87 tries" rather than 4. The tutorial keeps the guard that fixes it and explains the symptom, since it is easier to recognise than to describe. Both tutorials were compiled and played through headless, and their code blocks are checked by extracting and compiling every java block on the page.
Costumes were the biggest hole in the tutorials: not one of them ever changed a sprite's appearance, though Looks is a whole Scratch category. Make it Walk builds animation twice over. First by hand - two costumes and a timer - so it is plain that animation is nothing but pictures being swapped, and the reader can change the interval and watch what happens. Then with AnimatedSprite, which takes the pattern "alienGreen_walk%d" and does the swapping itself. It goes second, after the first program, because legs that move are the most rewarding thing a beginner can add and the built-in costumes are already named for it. The tutorial index was an empty page with a title. It now lists the five chapters in order with what each introduces, and notes that the first four need no downloads. While writing that page I claimed every documentation example is compiled when the site is built. It was not - I had been doing it by hand. DocumentationSnippetsTest now does it during mvn test: it pulls every java block out of the hand-written pages, keeps the last version of each class, and compiles them a page at a time so classes on the same page can see each other. Verified it fails by putting the old KeyCode.VK_RIGHT names back into the new tutorial and watching it report Walker.java:16, then pass again once restored. That guard matters because two tutorial examples had already rotted this way, unnoticed, when KeyCode became an enum. 152 tests pass.
Messages were the last whole Scratch category with no tutorial coverage. A bee, a ladybug and a snail race across the stage while a referee sign broadcasts "go" and "stop". The chapter is built around the thing that makes messages worth having: the referee never mentions a racer and no racer mentions the referee. Adding a fourth racer needs no change to the referee at all, which is the argument for broadcasting over holding a list of everyone to notify. It also brings in constructor parameters, which no earlier chapter needed. One Racer class makes all three creatures, taking its costume, its lane and its speed - and since every creature in the built-in library has a matching _move costume, creature + "_move" gives each racer its second picture for free. The speeds differ so the snail loses, which is both funnier and a better demonstration than three identical racers moving in lockstep. An aside on comparing text with equals rather than == is included, since this is the first chapter where a beginner compares strings. The tutorial index and chapter numbering were updated; the path is now six chapters, five of which need no downloads. Verified by playing the race headless: all three creatures start and stop with the broadcasts and finish in speed order. The documentation test was checked against this page specifically by breaking the broadcast call and watching it report Referee.java:17. 152 tests pass.
Each of the five tutorials now links a downloadable project with the completed code, following the bouncy-hedgehog-100 convention: the java files, an empty +libs for the jar, a .vscode settings file and an empty package.bluej so BlueJ opens the folder as a project. The archives are generated from the tutorial pages themselves, taking the last version of each class, so they cannot drift from what the chapter tells you to type. They serve two purposes: a learner whose code will not run can compare against something that does, and a teacher can check a chapter end to end without typing it out. Because these tutorials use only built-in costumes and sounds, each archive is between 1.7 and 2.3 kB - just source. The hedgehog project, which carries its own images, is 104 kB. Verified the way a student meets them: unzip the archive, drop the jar into +libs, compile with nothing else on the classpath, and run. All six archives compile against the shipped jar alone, and three were run headless with no warnings or exceptions beyond the graphics driver noise of the test machine. 152 tests pass.
The finished projects carried no main method, so the only way to start one was to right-click a class in BlueJ. That is what the tutorials describe, but the archives also ship a .vscode settings file, and a terminal is the obvious fallback when an IDE misbehaves - neither worked. Every tutorial's stage class now ends with a main, the way the Bouncing Hedgehog project always did, so the same code runs from BlueJ, from VS Code's Run button and from the command line. Since the archives are generated from the tutorial pages, the entry point was added to the pages and the archives regenerated from them. Each README now gives all three ways to start the project, with the Windows classpath separator spelled out separately, since that trips people up. Checked by following the README exactly: unzip the archive, copy the jar into +libs, javac with only that on the classpath, java, and confirm the window renders. 152 tests pass.
The projects under docs/archives could not be run without first building the
19 MB jar and placing it by hand.
An "archive" Maven profile now compiles a chosen project against the library as
it stands in the working copy and runs it, with no jar needed:
mvn -q compile exec:exec -Parchive -Darchive=make-it-walk-100 -Dmain=WalkStage
Two things had to be right for that. exec:java runs inside Maven's own JVM,
where the graphics library fails to load its native code, so it forks with
exec:exec instead. And the classpath has to be runtime scope rather than
compile: JOGL arrives as a runtime dependency, and with compile scope the
project starts and then dies on NoClassDefFoundError.
For BlueJ and VS Code, which do need a jar, scripts/link-jar.sh puts the built
one into every project's +libs, symlinking where it can and copying where it
cannot so Windows works too.
Those jars must not reach the published site. Measuring showed why: a copied jar
turns a 2 kB project download into 18 MB, though a symlink is not followed by
the archiver. Rather than depend on that difference, the jars are git-ignored
and build.sh removes them before the archives are zipped, reporting what it took
out and how to put it back.
Both routes are documented in the README, with a table of every project and the
class that starts it.
152 tests pass, and the profile was checked by screenshotting the window it
opens.
There are 21 demos, 123 reference examples and 9 finished projects in this repository, and running any one of them meant knowing its package, its main class and which classpath it needed. scripts/run.sh lists all 153 in an fzf picker with the source previewed beside them and runs whatever is chosen. Demos and reference examples run straight from target/classes, which already holds their assets. The tutorial projects under docs/archives are not part of the build, so they are compiled on demand into target/archive-run, with the archive folder itself added to the classpath so projects carrying their own images still find them. Reference examples kept in a folder are listed under the folder name. Labelling them by class name, as a first version did, showed a list of thirty entries all called MyWindow. --select-1 means a query matching exactly one thing runs without further interaction, which makes the script usable from another script as well as by hand. Documented in the README alongside the two ways of running the tutorial projects.
The 39 changesets accumulated over v5 are replaced by one, because the version workflow concatenates whatever it finds and that would have produced a changelog contradicting itself. Merging them surfaced four such contradictions, each now fixed rather than shipped: v5-constructors taught Window.FULL_SCREEN, a field v5-window-settings had already replaced with useFullScreen(). Worse, v4.28.2 never had that field at all - full screen was a constructor with a leading boolean - so the migration row a v4 reader needs is the constructor, not the field. Seven changesets each announced a running method count as their pass landed (218, 208, 199 ... 179), but later work added methods back. Counting both trees the same way gives 289 before v5 and 193 now. Three changesets blamed KeyCode becoming an enum for broken examples, which reads as a v5 break. It was already an enum in v4.28.2, so it is not in the migration guide. The new changeset leads with migrating from v4 rather than with the feature list, on the grounds that someone opening a major release wants to know what breaks first. Every API in it was checked with javap against target/classes, and every v4 name against efc04c1. Chapter 7, Dodge the Rocks, covers the one part of Window/Stage/Sprite no tutorial ever used. All six existing chapters were a single stage with a main, and the size in super(500, 260) was quietly creating a window as a side effect, while both starter projects hand the reader a MyWindow.java the book never mentions. The chapter separates the two and then spends the separation on a title screen, the game, and a game over screen that is the title screen again with a different message. The game deliberately reuses the walker from Make it Walk and the falling objects from Catch the Coins, so the only new idea on the page is the screens. Three bugs turned up while building it, all of which needed a scaled display to see, and none of which reproduce under Xvfb because there the window is exactly the render size: ifOnEdgeBounce repositioned a sprite from the size of its hitbox alone, which only holds while the hitbox is centred on the sprite. Since the default hitbox now wraps the painted pixels, alienGreen_stand - 109px of transparent padding above the character and none below - was moved to a spot still over the border, where it stuck, permanently a third off screen. It is now nudged by however far the hitbox pokes out. That is what the old make-it-walk-2.gif was showing. Setting a stage made the window flash: the letterbox bars came out in the sketch's default grey for exactly one frame. A render buffer's OpenGL framebuffer is allocated the first time it is drawn to, which fell after the canvas had been painted black. They are allocated first now. GifRecorder.stop wrote the GIF trailer before it stopped accepting frames, so a frame arriving from the render thread landed after end-of-file and the last frame came out truncated. The loading screen takes a logo through Window.useSplashLogo and fades in, then out over the first frame of the stage. The fade out cannot start until the fade in has finished, or a project with nothing to load - every tutorial - would show the logo blinking once at full opacity. Stage.stamp(Queue<Stamp>, Layer) existed only so TiledMap could reach the stage's stamp queues from its own package, which put a method taking a type nobody outside the library can build next to the real blocks in autocomplete. It goes through internal.StageAccess now and is package-private: the same trick as StageHooks, in the other direction. Catch the Coins put its score at x=-270 on a 600-wide stage, but new Text() centres on its position, so the live GIF showed a clipped "Coins: 1". Centred at the top like chapters 5 and 7. The library default is documented and twenty call sites are positioned around it, so it was left alone. CLAUDE.md claimed there is no test suite. There are 152 tests, and DocumentationSnippetsTest is what tells you which tutorial a public API change just broke. mvn test passes. The tiled demo was run to confirm the stamp bridge still draws its map, and the re-recorded GIFs were checked frame by frame.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.