Main Content

Results for

I thought I had the game in pretty good shape until I tried it in MATLAB Online. A launch that felt immediate on desktop could feel slower in the browser, which matters in a game where timing is the main control.
I built Slingshot Home: Escape Saturn for the MATLAB One-Button Game Mini Hack. The goal is to time your launches to travel between 15 of Saturn’s moons and return to Earth, with three shields and a clock counting down.
Getting the game to feel responsive took a few iterations, particularly when running it in MATLAB Online. Here are some of the approaches that worked well.
Reuse Graphics Objects
The game creates 21 graphics objects during initialization and reuses them throughout gameplay. Instead of recreating the scene, it updates properties such as position, color, and text.
I also check whether a property has changed before assigning it again. Static planets and labels do not need to be updated as frequently as the spacecraft.
For the Mini Hack, the game harness handles the window, input, and drawing loop. The game provides the init, update, and render functions.
Use Elapsed Time for Motion
The spacecraft moves around each moon using basic trigonometry:
theta = startAngle + angularSpeed*elapsedTime;
position = moonCenter + radius*[cos(theta), sin(theta)];
heading = [-sin(theta), cos(theta)];
Pressing the button launches the spacecraft along the tangent to its current orbit, so the gameplay is based on timing rather than steering.
Calculating the orbital angle from elapsed game time, rather than advancing it by a fixed amount per frame, also makes the motion less dependent on rendering speed.
Separate Trajectory Calculation from Rendering
I used ode45 to calculate each flight trajectory, then deval and interpolation to provide positions during playback. This avoids solving the trajectory again on every frame.
The physics is intentionally simplified. The moons remain in fixed positions, captures are assisted, and the escape gate represents leaving Saturn’s sphere of influence rather than calculating its actual boundary. These assumptions are documented in the code comments.
Generate Textures in MATLAB
The planet textures are generated directly in MATLAB, so there are no external image files to load:
MATLAB
x = linspace(-1,1,80);
z = max(0,1-x.^2-x'.^2);
brightness = max(.1,.8*sqrt(z)-.6*x);
This produces directional lighting while retaining some brightness on the night side. The textures are generated once during initialization.
Increasing the texture resolution helped smooth the edges, but adjusting the lighting had a larger effect on the overall appearance.
Test as You Go
I used Codex to help edit and refine the code. Through MATLAB MCP, it could run MATLAB code, inspect Code Analyzer results, and exercise states such as countdown, launch, and restart.
That was useful for finding issues, but running and playing the game was still important. Some things only became apparent during gameplay, such as a launch feeling delayed or an orbit being difficult to see. I also tested regularly in MATLAB Online rather than relying only on desktop behavior.
I used the Mini Hack validator to keep track of the character limit:
minihackContest.validate
The validator count differs from the raw file size. Comments are excluded, so I could document the physics and assumptions without using the executable-code budget.
Try It in MATLAB Online
One feature I liked about the final result is that there is very little setup required to try it.
Click “Play game,” then “Run this app,” and the game runs in the browser.
For me, the biggest improvements came from fairly small changes: reusing graphics objects, basing motion on elapsed time, calculating trajectories separately from rendering, and testing regularly in MATLAB Online.
FOLD: One Button, Two Worlds
For the MATLAB One-Button Game Mini Hack, I developed FOLD, a six-level puzzle-platformer built entirely in MATLAB.
Unlike traditional platformers, the player does not directly control the character. Instead, the character moves automatically, while a single button changes the geometry of the environment.
Each press rotates platforms, changes available paths, and activates or deactivates obstacles.
Play FOLD here:
FOLD — Rotating platforms and phase-based obstacles controlled with a single button.
1. Creating Rotating Platforms with Trigonometry
One of the main technical challenges was creating platforms that rotate around a fixed pivot.
Rather than recreating graphical objects every frame, I calculate the vertices of each platform using trigonometric functions.
For a platform with length L, thickness w, and rotation angle a:
dx=L*cos(a);
dy=-L*sin(a);
nx=w*sin(a);
ny=w*cos(a);
px=[x x+dx x+dx+nx x+nx];
py=[y y+dy y+dy+ny y+ny];
These coordinates define the four corners of the platform.
By updating the XData and YData properties of an existing patch, MATLAB can animate the platform without creating new graphics objects every frame.
This approach keeps the animation simple and efficient.
2. Making the Character Move with a Platform
Initially, the character would fall whenever a platform started rotating.
To solve this, I introduced a mechanism that temporarily attaches the character to the rotating platform.
The character's position is calculated relative to the platform's pivot:
s.x=170+s.dist*cos(a)-12;
s.y=440-s.dist*sin(a)-24;
Here, s.dist represents the distance along the platform, while a is the current rotation angle.
This allows the character to move together with the platform and reach elevated sections of the level.
3. Designing Gameplay Around a Single Button
The most interesting design challenge was creating meaningful gameplay with only one input.
FOLD uses two world states: A and B.
Pressing SPACE or clicking the mouse switches between these states. Multiple platforms respond simultaneously, while certain obstacles become active or inactive.
This creates situations where opening one path may close another.
Across six levels, players must learn to anticipate how their actions affect the environment.
The objective was to make the game progressively more strategic without introducing additional controls.
4. Optimizing MATLAB Code for the Character Limit
The competition limits submissions to 10,000 characters.
One important discovery was that the validator's character count could differ significantly from the raw source file length.
Removing whitespace alone was not sufficient.
I reduced code duplication, reused helper functions, represented level configurations using arrays, and simplified unnecessary graphical elements.
The final implementation successfully passed minihackContest.validate.
5. What I Learned
Developing FOLD helped me explore several useful MATLAB techniques:
  • Updating existing graphical objects instead of recreating them.
  • Applying trigonometry to interactive 2D geometry.
  • Managing game states and frame-based updates.
  • Designing multiple puzzles around a single input.
  • Optimizing code under strict technical constraints.
AI assistance was used during prototyping, debugging, and code refinement. The gameplay was tested locally in MATLAB, and the final submission passed the official contest validator.
The main takeaway: limitations can encourage creativity. With a single button, basic trigonometry, and carefully designed state transitions, it is possible to build an interactive puzzle game with surprisingly varied gameplay.
My second entry, Event Horizon (entry 16546), puts you in orbit around a black hole. Tapping fires your engine and climbs, and the accretion disk's drag pulls you down. Lower orbits slow your clock, so your score is how many years into Earth's future you travel. Flares light up rings of the disk, and you have to get out of a red band before it turns white and burns.
Real frames from the game. In the top left, the ship (cyan, marked YOU) is behind the hole, and gravity bends its light into an arc over the top.
The picture isn't drawn by hand. It comes from tracing light through the Schwarzschild metric, the spacetime around a non-spinning black hole. Here is what I learned building it, again with help from an AI coding assistant (Claude Code), testing each change in MATLAB.
1. Trace the light once, then shade the map every frame
My first prototype traced one light ray per pixel every frame. With rays bending around the hole, that took about 240 ms per frame at 160 by 120 pixels, far over the 33 ms a 30 fps frame allows.
The fix was to notice that the camera never moves. Each pixel's ray always lands in the same place, so I trace every ray once in init and store where it crosses the plane of the disk. In these units, with the Schwarzschild radius set to 1, a light ray's path follows a simple force law, which I step for all 19,200 rays at once:
h2 = sum(cross(P,V).^2); % each ray's angular momentum squared
for k = 1:150
d = min(.07*vecnorm(P),1); % smaller steps near the hole
Q = P+V.*d;
V = V-1.5*h2.*Q./vecnorm(Q).^5.*d;
% ...record where each ray crosses z = 0, drop rays that escape or fall in
end
Each ray keeps its first two crossings of the disk plane. The first is the direct view. The second is light that went around the back of the hole, which is why you can see the far side of the disk both above and below the shadow.
The trace takes about 160 ms at startup on my PC. After that, each frame only shades the stored points, and the game's whole frame work is about 6 ms.
The 240 ms comes from an early prototype with a slightly different camera, so treat the ratio as approximate.2. Paint the game into the disk, and gravity bends it for free
Because the map stores a disk position for every pixel, anything I draw in disk coordinates gets lensed automatically. The ship is just a glow centered on its position in the disk, and the flares are rings of disk radius:
zs = s.r*exp(1i*s.ph); % ship position as x + iy
S(L.i) = S(L.i)+exp(-abs(L.c-zs).^2/.2); % L.c: disk point of each pixel
b = max(0,1-((L.r-s.fc)./s.fw).^2); % flare rings by radius
When the ship passes behind the hole, its image smears into arcs above and below the shadow. No extra code does that. The engine exhaust from each tap is drawn the same way, so it bends too.
Storing disk points as complex numbers kept the distance check to one short line.
3. Real physics needed a real gauge
The orbit uses the Schwarzschild radial equation, so the innermost stable orbit at r = 3 is real: below it, no orbit is stable and the ship slides inward. Time dilation sets the score. A circular orbit's clock runs at sqrt(1 - 1.5/r) of a distant clock, so Earth gains the rest:
s.vr = s.vr+(-.5/s.r^2+s.L^2/s.r^3-1.5*s.L^2/s.r^4-.04*s.vr)*T;
s.yrs = s.yrs+10*h*s.m*(1-sqrt(max(0,1-1.5/s.r)));
The physics made two first designs fail:
  • Head-on debris didn't work. Moving up or down is slower than going around the hole, so you met debris before a burn could lift you clear. Climbing one unit took about 3 s even tapping 5 times a second. I replaced debris with flares that warn for 3.5 s, and I sped up game time 1.6 times.
  • A beautiful view isn't a readable one. In my first playtest, I couldn't tell what a tap did, because the lensed ship jumps around the screen. A simple height gauge on the left fixed that. Tapping moves your bar up, red bands are flares, and green bands are bonuses. The black hole is the show, and the gauge is the controller.
After that came the parts that made it fun: a combo multiplier for staying deep, stages every 100 years with faster and drifting flares, and a close-call bonus for clearing a flare by a hair.4. Small tricks that saved characters
The final file counts 9,993 of 10,000 characters. The counter reformats your code first, so comments are free but spaces and indentation count, as I described in my first post. These helped:
  • Divide by zero to hide things. 0/0 is NaN, and graphics skip NaN points, so one expression both positions and hides a marker:G.gs.YData = [1 1]*(100+60*(s.r-1))+0/(s.mode == 1);
  • Make several texts in one call. text takes vectors and a cell array of strings:text(ax,[103 108 50 50],[220 280 88 502],{'ISCO','COMBO','HORIZON','HEIGHT'},f{:});
  • Store end times, not countdowns. A message that shows until s.t < s.got needs no line to count it down each frame.
  • Write constants out when they're fixed. My camera never moves, so its right and up vectors became constants instead of cross products.
One trim I had to undo: dropping a normalization in the star placement put stars inside the black hole's shadow. Look at the frames after every trim.Play it
If MATLAB Online is slow for you, run it in desktop MATLAB: download the starter project, copy the entry's code into game.m, and run minihackContest.play.
How many years into the future can you get?
Lira vanished 19 years ago. Now her signal is back. You control a radio telescope that sweeps the sky, and you only have one button (SPACE or click).
  • Watch the dish sweep left and right. When the beam passes over a “?” in the sky, press to lock onto the signal.
  • Then tap as fast as you can to collect pulses (up to 8 in 2.4 seconds).
  • Each signal turns out to be an ALIEN, a NEW PLANET or a BLACK HOLE, and a little face appears in the sky.
  • Lira’s signature is SHORT-SHORT-LONG. Aliens repeat it, so they need more pulses to be confirmed. If you don’t reach the goal, the alien gets away and you can try again.
  • Every alien you confirm decodes part of a hidden message. Clear the three sectors (Pulsar, Dust Cloud, Deep Space) to read it.
HOW THE CODE WORKS
The game follows the contest contract: game() returns init, update and render.
1. init – builds everything once.
It draws the pixel-art backdrop (one small image with a sunset in color bands), the dish, the wave panel, the texts and the three faces. Each face is a few ellipse patches inside an hggroup, parented to an hgtransform so it can be moved and scaled later.
2. update – the game logic.
The game is a small state machine with one phase number: 0 ready, 1 scan, 2 collect, 3 transit, 4 done. The same button means “start”, “lock on” or “tap fast” depending on the phase. A short cooldown after each press avoids double hits.
The dish angle is a triangle wave over time:
a = 110*(1-abs(mod(t*rate/110,2)-1))-55
A target can be locked when it is within 14 degrees of the beam. Each sector raises the sweep rate.
3. Signals and their type.
Each signal is built from two 3-letter patterns of short (S) and long (L) pulses, drawn as one waveform. The type comes straight from the patterns:
  • both halves equal to Lira’s SSL → alien
  • both halves equal but different → new planet
  • halves different → black hole
So the wave you see on screen really tells you what you found.
4. render – only updates properties.
Nothing is created while playing. Each frame only changes XData/YData of the dish and wave, the String and Color of the texts, and the Matrix of each face (makehgtform with translate and scale). The faces “pop” on every tap with a simple scale change. This keeps the game smooth.
5. Fitting under 10,000 characters.
The validator counted more than the file size, so I used anonymous helpers for repeated line/text calls, stored shapes in small matrices and ran minihackContest.validate often.
Thanks for playing, and good luck everyone!
I built Beat the PID for the One-Button Game Mini Hack: Beat the PID. Each tap switches an actuator on or off, and you try to hold a trace inside a moving band across six plants, from a thermostat to an inverted pendulum. A PID ghost plays the same level with the same on/off limit.
The yellow line is you, and the dashed gray line is the PID ghost. The band scrolls in from the right so you can plan ahead.
These are the things that surprised me, with measurements where I have them. I built it with help from an AI coding assistant (Claude Code), and we tested each change in MATLAB.
1. The character limit counts reformatted code
My first local count, with comment lines removed, said 9,862 characters. minihackContest.validate said 12,253. The validator's number matched this exactly:
code = char(tree2str(mtree(fileread('game.m'))));
numel(code)
mtree and tree2str are undocumented, so treat this as a useful local estimate. It matched on my game at several sizes and on another contest entry.
Reformatting changes what costs characters:
% What you write
ax.Color=[.035 .045 .07];
% What gets counted
ax.Color = [ .035, .045, .07 ];
  • Comments are free, so comment generously.
  • Squeezing out spaces saves nothing, because they get added back.
  • Every value in an array costs extra for its comma and spacing.
  • Each nesting level costs 4 characters per line. Moving a deep block into its own function can pay for itself.
  • Grouping assignments helps:
[s.t,s.acc,s.in,s.ing,s.ei,s.dwg,s.n] = deal(0);
Those changes, along with cutting a few visual extras, took my game from 12,253 to under 10,000 and kept all six levels.2. min and max ignore NaN
I used NaN for trace samples not recorded yet, then clamped values to the scope:
max(-.02,min(1.02,NaN)) % returns 1.02, not NaN
Because min and max ignore NaN, every empty sample became a real value at the top of the scope and drew a stray line. If you need clamping, restore the gaps afterward with y(isnan(v)) = nan.3. Use a fixed physics step and expm
dt varies between frames, so I run the physics at a fixed 120 Hz and spend the elapsed time in whole steps:
s.acc = s.acc+dt;
while s.acc >= s.h && s.t < s.T && ~s.crash
s.acc = s.acc-s.h;
s = stepPlant(s);
end
Each plant is linear, x' = Ax + B[u;1], so I can discretize it exactly once at startup:
n = size(A,1);
M = expm([A B;zeros(2,n+2)]*h);
Ad = M(1:n,1:n);
Bd = M(1:n,n+1:n+2);
Each physics step is then just x = Ad*x + Bd*[u;1]. The contest player limits dt to 0.05 s, so a game running under 20 fps plays in slow motion, but the physics stays stable and each step stays the same.4. A one-button PID is a relay
The ghost's PID output doesn't set a power level. Its sign picks on or off, and a minimum switching interval stops it from toggling faster than a person could:
e = r-y;
ei = max(-.3,min(.3,ei+e*h));
v = e+Ki*ei-Kd*(y-yPrev)/h;
if timeSinceSwitch >= minDwell && (v > 0) ~= u
u = v > 0;
timeSinceSwitch = 0;
end
The integral term matters more than I expected on a relay. On the inverted pendulum level, the ghost averaged 52% of the time in the band with Ki = 0.2, and 7% with Ki = 0. These are averages from simulated runs at one set of gains, and the result is very sensitive to tuning.
5. Measure work and frame rate separately
MATLAB Online was by far the biggest performance surprise. On my connection, the same game behaved very differently:
Median game and drawing time per frame. I measured MATLAB Online on a slightly earlier build, before the final color and text changes.
The MATLAB Online time was almost the same on all six levels, even though each draws different things, so I suspect a fixed delay and not my drawing load. Other games lagged for me there too. Your results may differ, so test your own game there.
Two habits helped:
  • Time the work, not just the frame. On my Windows PC, a paced loop took about 46 ms per frame even when the actual work was about 10 ms, because pause overshot. Frame rate alone would have sent me after the wrong problem.
  • Send only what changed. Create graphics once, set fixed X data once, and update only Y data. My render also skips frames where nothing changed, which keeps idle menus quiet and still gives the same result if called twice:
st = [s.mode s.lv s.t s.u];
if isequal(s.ax.UserData,st)
return
end
s.ax.UserData = st;
A piecewise-constant band needs vertices only at its ends and steps, which cut it from up to 166 numbers per update to a handful. Scrolling the scope at 15 Hz while moving the player marker every frame also reduced updates without making the controls feel slower.Play it on the desktop
If the game crawls in MATLAB Online, you can run it in desktop MATLAB:
  1. Download the starter project, unzip it, and open minihackContest.prj. Opening the project installs the contest tools.
  2. Copy my entry's code into the project's game.m.
  3. Run minihackContest.play.
Tap Space or click to play. I'd love to know which plant beats you, and whether anyone can beat the PID on all six.
By SunHuilin
What happens when a tiny pixel cat tries on eight different personalities? Sometimes you get instant cuddles. Sometimes your sofa gets a new ruler.
Purrfect Fit is my entry for the MATLAB One-button Game Mini Hack. Different pairs of eyes scroll through a cat's glasses. Tap or press SPACE to stop them. Fit the eyes inside the frames, and the kitty reveals its personality with a short message and matching pixel effects. Miss the target, and the kitty offers a little encouragement. Tap again to try another round.
Play the game: Purrfect Fit
Here are four techniques behind the game. The snippets below are simplified illustrations of the implementation, rather than a complete runnable game.
1. Treat an RGB array as a tiny canvas
The artwork starts as a 150-by-200-by-3 RGB array, displayed across an 800-by-600 game area. One artwork pixel occupies four game-coordinate units in each direction. Rectangles, filled polygons, and small bitmap patterns build the cat, glasses, lettering, and effects without loading external image files.
The key is to create the image object once, then update its CData:
% During initialization:
h = image(ax,[2 798],[2 598],background);
​
% During rendering:
frame = background;
% Draw the moving elements into frame here.
h.CData = frame;
The static background is prepared during initialization. Each render starts with that background and adds the animated elements. This avoids accumulating drawing operations from previous frames and keeps the number of graphics objects fixed.
I also draw the title with small bitmap letters. That gives the main heading a consistent pixel style without depending on an installed pixel font. The longer instructions and feedback use MATLAB text objects for readability.
2. Separate scrolling, stopping, and feedback
The game has three states: scrolling, success, and miss. The same button stops the eyes during scrolling and starts another round after the result.
Scrolling uses elapsed time rather than a fixed distance per update:
phase = mod(phase + speed*dt,192);
There are eight eye designs, each occupying a 24-row tile, so the strip repeats after 192 artwork pixels. Using dt makes the intended scrolling speed independent of the number of updates per second, although a slow display can still make fast motion harder to judge.
One small detail matters: when a press arrives, I check the current phase before advancing the strip. This keeps the timing decision tied to the state that was just rendered.
The fit test uses the visible vertical bounds of each eye design. Both bounds must lie inside the target opening; checking only the center could accept eyes that still stick out of the frame. On success, the strip snaps to the centered position for the final portrait.
A brief restart guard prevents an immediate second press from dismissing the result before the player can read it.
3. Animate the tail with a small coordinate shift
The cat's tail uses filled polygons. Instead of storing multiple animation frames, I shift their horizontal coordinates with a sine wave:
d = 3*sin(t*3);
x = x + round(d*(118-y)/21);
The vertical weighting keeps the base relatively steady while the tip moves more. Rounding preserves the pixel-art appearance.
Layering is just as important as motion. I draw the moving tail first, restore the static foreground artwork through a mask, and then draw the eyes and glasses. This keeps the tail behind the body and prevents it from painting over the face.
4. Store small patterns compactly, then reuse them
The eight eye styles share a palette and a common rendering path. Their pixel data is stored as compact runs and decoded during initialization, so rendering can reuse the prepared arrays.
The personality effects use a different representation: seven integers describe a 7-by-7 binary pattern. Each bit says whether one pixel is filled:
if bitget(pattern(row),8-col)
% Draw this pixel using the personality's color.
end
The same drawing loop produces paws, lightning bolts, sleepy Zs, sparkles, spirals, question marks, hearts, and musical notes. Changing the pattern and color gives each personality its own visual feedback without duplicating the effect renderer.
The messages work similarly: each eye style has three possible success lines, while misses draw from a shared pool of encouraging phrases. For example:
  • Fierce: "Tiny paws. Big boss energy."
  • Sleepy: "Wake me when snacks arrive."
  • Loving: "Can I keep you, human?"
  • Miss: "Oops! Let's blame the whiskers."
Random choices happen when a round starts or a result is selected. Rendering reads those choices, so the message does not change every frame.
What helped most under the character limit
The biggest savings came from reusing drawing routines, palettes, and animation logic, and from compact representations of the artwork. Shorter variable names helped too, but shared structure saved more space than shortening names alone.
The current build passed the official validator at 8,933 counted code characters. During compression, I compared rendered frames and seeded update sequences with the previous version to check that the smaller code preserved the existing appearance and gameplay.
My favorite part is that the eyes change more than the cat's expression: they change the little conversation you have with it.
Which personality would you add next: a suspicious detective kitty, a dramatic movie-star kitty, or something even stranger?
Development note: I used OpenAI Codex to assist with coding, iteration, and code review.
I have to say, fitting this into 10k characters was quite challenging. This is an original version coded many, many years ago that I wanted to reimplement to test my limits. I actually lost the code of the original project because the PC died...and since at the moment I feel nostalgic for that achievement I just wanted to go back my steps.
First of all: colours and textures. To stay in the limits I had to give up on them.
Moreover, the implementation of the collision system definitely had to be simplified -guys be kind in the comments please- and reused for both the walls and the moving parts of the flipper. Of course it does not handle edge-cases and sometimes the ball travels where it should not. By the way, I think the real advantage of this 'reuse' approach is that the elasticity of each collision can be independently parameterized. And also, it's a simple Flipper game with one button, it's just for fun, no real physics, just creativity. Since I'm naturally competitive, I deliberately decided to make this one relaxing: no score, no pressure, and infinite lives. 💚
  • of course the ball moves because at some point I say (x,y)=(x,y)+v*t and v=v+a*t,
  • and of course I had to pack the more verbose geometric formulas (semi-obscured tilted ellipses 💀) with some clumsy function-handle
  • ⬆ but this is totally off-topic.
About the flippers, I loved the idea to save some little extra space by rendering all the moving segments with the quiver function, since the junction point (x,y) in this game is fixed and calculating the relative (u,v) component comes for free.
I really had to abandon the wormholes (a kind of segment that if the ball enters it - exits from another portal) but at least I could recreate a weird kind of solar system in which any planet is used as a bouncer. (This year I got married 💍, so I plot many many saturn-rings 🪐- I love you Selena)
In the original game there were some conditions in which the gravity could flip direction (bottom-up) and this feature was smartly comunicated in-game with some asteroids that changed orientation (upside down) based on the situation. Kinda delightful, but the first rule of the game is clarity for the player: if it's too complicated to put inside and the code is already saturated, better let it go.
But what I really appreciate of such a strong language as matlab happens to be, are the small details in the classic syntax. Do you recall this simple code? ⭐ plot( ___ , 'p' ) ⭐ that generates star-symbols at no cost! The temptation to use it on space-themed works is SO STRONG!!! Couldn't resist. It's too cool! And to make the idea of an infinite flow of stars there is a buffer of coordinates that keeps resetting those ones that reach nearly the center of the black hole, so there is no need to delete and regenerate new symbols. Some coding expert probably would label it as Procedural Generation... not my case: it's just common sense - MATLAB Online renders so slowly 🥲
I guess I said enough... So, enjoy the Black Hole in the space flipper, a simple state-machine mechanic that when the ball passes too close makes the hole star-hungry and when it reaches the critical level, all the universe vanishes and all the trajectories collapse due to the gravity, until the timer resets the state 🫠. I'm not sure but Matthew Mcconaughey probably was in that tiny Hole during the first rendering step, I swear I saw him...!
Don't forget to smile for the right reasons. Byeeee
clc, clear all
(non AI generated text, the use of emojis was a built-in feature of my personality)
For the MATLAB One-Button Game Mini Hack I made KOI — and I fell straight into the trap: I skimmed the rules, got excited, and built the prettiest game I could (caustic water shaders, a spine-chain fish, score-reactive effects…). Then I met the second line of the rules: 10,000 characters. The next several hours were spent tearfully ripping out features and compressing code until no human could read it anymore.
Read the requirements first. The constraints are not the fine print — they are the design brief. "Beautiful" and "10k characters" lead to two very different first lines of code.
A thank-you to AI agents, by the way: my final file has dense comments and aggressively packed one-liners that a human reader would weep at — but the agent working alongside me still knew exactly what was going on, measured every cut before it was made, and caught every regression. If you use agents for this, a few things made a real difference:
  • Clear structure — small, single-purpose functions (init / update / render / helpers) instead of one blob
  • Skills and rules — giving the agent your constraints (contest contract, banned functions, testing workflow) explicitly, once, up front
  • Sorted context — snapshots of earlier states on disk meant we could always trade features back in; nothing was ever lost, only parked
If I did it again, my order of work would be:
  1. Brainstorm — everything on the table, nothing wrong yet (a pretty game and a minimal game are both valid answers here)
  2. Decide what you actually want — the experience, not the feature list
  3. Write down requirements — including the contest rules, this time as step one
  4. Architecture — how state flows, what gets created where
  5. Individual base functions, then merge the ideas in
  6. Test headless from minute one
  7. Friends play it — they find what you're blind to
  8. Only then: polish
I did steps 4–8 with a lot of joy and steps 1–3 not at all. That order is free, and it would have saved me a very entertaining, slightly painful afternoon.
GLHF — and read the rules. 🙂
PS:
Idea:
Final result after cutting dynamic rock spawns, lilies on the leaves, drifting light particles, guide lines, wake ripples, a second pickup ripple, 3 of 4 title glow copies, the game-over box styling:
PPS: but it still looks beautiful though haha
My entry for the one-button game mini hack, GOING UP, makes you the lift attendant. The lift never stops on its own: one tap stops it, the next tap sends it on. Every passenger wears the colour of the floor they want.
How to play
  • Stop level with a floor and the gate opens. Riders for that floor get off (1 point each), and waiting people get on until the lift holds four.
  • Stop more than 12 px off a floor and you get a CLUNK. Nobody moves, and everyone waiting loses 1.5 s of patience.
  • Stop within 4 px while someone gets on or off: PERFECT, +1 point.
  • Waiting passengers have a patience bar. Three walk-outs and your shift is over.
  • The lift speeds up and people arrive faster as the shift goes on.
Tips for a high score
  • Skip floors with nothing to do. Every stop costs time.
  • The lift only turns around at the top and bottom. Pick up people whose floor is ahead of you; anyone else rides to the end and back.
  • Don't mash. In my bot tests random tapping lost in about 21 s, and never tapping in about 29 s.
MATLAB tricks inside
  • The lift has no direction flag. Its height is a triangle wave of the distance travelled, cy = 460 - abs(mod(ride, 840) - 420), so stopping just pauses ride.
  • Queue order without loops or sorting: ahead = sum(w' & s.pf' == s.pf & s.side' == s.side & s.pt' < s.pt, 2) counts, for every passenger at once, who arrived earlier at the same spot.
  • Timers store when something happened (s.stopAt = s.t) instead of counting up every frame. Elapsed time is just s.t - s.stopAt.
  • All 20 passengers are one patch of 6-vertex bodies plus one scatter of heads. Empty slots are NaN, so nothing is created after init.
What's your best shift?
My entry for the one-button game mini hack, LIGHTKEEPER, puts you in a lighthouse on a reef. Ships sail in blind from the dark. The beam sweeps on its own, and one tap reverses its spin. Any ship the light touches turns for home.
How to play
  • A tap reverses the beam. It has inertia, so a reversal takes about a third of a second.
  • Boats are rescued the moment the light touches them: 1 point.
  • Freighters need 0.6 s of light: 3 points. The arc around the ship shows progress.
  • A lantern blinking red means that ship is close to the reef. Three wrecks and the lamp goes out.
Tips for a high score
  • Tap fast to make the beam hover. Quick reversals cancel out, and a slow beam lights freighters three times faster.
  • Swing toward the ship closest to the reef, not the one closest to the beam.
  • Don't mash. In my bot tests, random tapping lost in about 19 s, and never tapping in about 39 s.
MATLAB tricks inside
  • Every position is a complex number, so z.' + outline.*exp(1i*heading.') turns and places the whole fleet at once. Use .', because ' also conjugates.
  • All ships are one patch with a face per ship. NaN vertices hide the ones not sailing. The whole game is 24 graphics objects.
  • The beam is a patch with per-vertex alpha ('FaceAlpha','interp'), built once and turned with an hgtransform.
  • Setting the axes limits every frame, even to the same values, added about 5 ms to each frame. The screen shake now touches the axes only while it's running.
How long can you keep the lamp lit?
Hi everyone! I made PULSE // PALS - Lumi & Afterglow for the One-Button Game Mini Hack.
Play the published game:
https://www.mathworks.com/matlabcentral/communitycontests/contests/10/entries/16481
Press SPACE or click when a diamond reaches the small ring beneath Lumi. Release between taps. There is no aiming or dragging. The game has 64 authored notes across eight visual phrases, with a tempo that gradually rises from 96 to 136 BPM.
The submitted version passed minihackContest.validate at 9,778 characters, with no banned functions reported. Here are the most useful lessons from building it. The short code examples below use descriptive names for explanation; they are not additional files required to play.
1. Make the moving target and the notes share one beat coordinate
The target drifts and the rail bends, but the input is still about timing. A note must meet the ring at its scheduled beat, wherever the ring has moved.
For a note at noteBeat and a current beat b, define d = noteBeat - b. This simplified example uses the same rail formula as the game:
rail = @(d,b) 22*(sin(.72*d+.22*b)-sin(.22*b)) ...
+ 26*sin(pi*b/12).*tanh(d/2);
b = 12.5;
noteBeat = 14;
targetX = 218 + 58*sin(pi*b/8);
targetY = 306 + 37*sin(pi*b/6);
spacing = 91*(1+.09*sin(pi*b/4));
d = noteBeat - b;
noteX = targetX + spacing*d;
noteY = targetY + rail(d,b);
The useful invariant is rail(0,b) = 0. At d = 0, the note and target coincide. The hit test uses beat distance, rather than mouse position or a pixel collision. That keeps the animation and timing rule connected while leaving room to change the path.
2. Animate a character's parts, then batch the drawing
Lumi is procedural geometry, not an imported sprite. The ears, hands, blinking eyes, star pin, and two scarf tails have separate motion. A shared transform adds tilt and squash-and-stretch.
All 22 colored character polygons are held in one patch object; the mouth uses one additional line. Independent animation does not require an independent graphics object for every body part.
Complex coordinates are a convenient way to express 2-D transforms. For local points z, rotation angle theta, and a position x0,y0:
zWorld = (x0 + 1i*y0) + z*exp(1i*theta);
set(h,'XData',real(zWorld),'YData',imag(zWorld));
The game creates its graphics objects during initialization and reuses them. It also avoids setting text when the string has not changed:
if ~strcmp(label.String,newText)
label.String = newText;
end
The main waveform uses 177 on-screen samples instead of storing a whole song-length curve in the scene. Background geometry has a separate update gate. These reduce unnecessary work; they are not a promise of a particular frame rate in MATLAB Online.
One testing lesson: check the effective face colors, not only one expected CData array shape. With flat shading, per-face colors and per-vertex colors need to be interpreted correctly; for the latter, the first vertex determines the face color.
3. Make the strongest effects something the player earns
A catch makes Lumi rebound and produces a ring around the target. Eight consecutive catches outside Afterglow fill the constellation and start an eight-beat double-scoring interval for subsequent notes. The palette warms, the scarf extends, and the surrounding shapes become stronger.
I wanted the visual changes to explain what the player had achieved. The background stays comparatively restrained between these moments so that the next diamond remains readable. The chart is fixed rather than randomized, so replaying also means learning its pauses and short runs of notes.
4. A slow frame is not proof that the player left
An earlier version paused whenever an update interval exceeded 0.8 seconds. During MATLAB Online playtesting, it repeatedly paused just after starting. The code was treating elapsed time as if it were a window-focus event.
I removed that automatic-pause condition. The submitted version bounds a simulation step to 0.1 seconds and marks slow sessions PRACTICE. This avoids a large catch-up jump, but intentionally allows the game to run slower than wall-clock time under sustained load. It does not fix rendering throughput or network latency.
A browser prototype helped with visual iteration, but browser smoothness is not a measurement of the MATLAB version. Testing in the official runner caught a problem that screenshots and reference simulations did not.
5. Pack data, and let the official validator decide the size
Some geometry, palette, and chart tables are stored as character arrays and decoded into numbers. For a small integer table, the principle is:
values = [0 2 4 6];
packed = char(13312 + values);
restored = double(packed) - 13312;
assert(isequal(values,restored));
Those unusual-looking characters are numerical data. They are not dynamically executed code. Keep values in a safe character range, preserve the source encoding, and verify that decoding reproduces the original table.
The distinction between character arrays and string scalars matters: double('ABC') returns character codes, while double("ABC") attempts a numeric conversion and returns NaN. My decoder receives character arrays.
Raw file size was not a reliable way to judge the contest budget. I used minihackContest.validate for the final decision and kept an expanded numeric copy for inspection. Compact data, shared drawing helpers, and fewer repeated statements were more useful than merely deleting whitespace.
Working with AI
AI assistance was used for implementation, iteration, and testing support. I playtested the game and used visible problems to steer changes: the first designs were too static, the character felt like a sticker, and the slow-frame pause rule failed in actual use. The important lesson was to keep reference-model checks, native validation, and real playtesting separate.
Useful MATLAB documentation
Graphics performance:
https://www.mathworks.com/help/matlab/creating_plots/improve-graphics-update-performance.html
Patch colors and properties:
https://www.mathworks.com/help/matlab/ref/matlab.graphics.primitive.patch-properties.html
Character codes and numeric conversion:
https://www.mathworks.com/help/matlab/matlab_prog/unicode-and-ascii-values.html
What visual cues help you anticipate a beat when there is no sound? I would be interested to hear what other Mini Hack participants have tried.
One-Button Game Mini Hack Entry: Pump-Send
Hi everyone! Pump-Send is a side-view mountain bike game on an endless desert pump track. You never pedal: all your speed comes from pumping the rollers, and one button does everything. On the ground, a tap pumps or pops; in the air, it throws you into a backflip.
How scoring works
  • You get 1 point for every meter you ride. The run ends if you run out of momentum or crash.
  • Every clean backflip is worth 50 points, so one flip equals 50 meters of riding.
  • Your score and flip count are shown in the top-left corner.
Definitions
  • Pump: A tap while rolling downhill. You push the bike into the slope and gain speed.
  • Scrub: A tap at the wrong time (going uphill, or a second tap right after a pump). It costs you a little speed.
  • Lip: The rising face just before the top of a hill. It glows yellow.
  • Pop: A tap on a glowing lip. It's the only way to get airborne.
  • Flip: A tap in the air spins the rider backward. Each full rotation you land counts as one flip. Tap only; don't hold the button.
  • Stall: Going too slow for too long. This ends the run.
Physics you'll want to know
  • Steeper downhill = bigger pump. A pump on a steep drop gives you a big shove, about a third of your starting speed. On a nearly flat stretch, it does almost nothing. Aim for one good pump on each downhill.
  • Scrubs add up. A badly timed tap only takes back about a third of a good pump, but a few in a row can leave you crawling.
  • Watch the hills scroll. If the scenery slows down and you're barely making it over the crests, pump now or you'll stall.
  • Speed is hang time. The faster you hit a glowing lip, the higher and longer you fly, and the more time you have to finish a flip.
  • Spin fades in the air. Each tap adds spin, and the spin slowly dies down. More taps spin you faster, but you have to be wheels-down at the right angle when you touch the ground.
  • Land matching the slope. Don't start another flip if you won't finish it in time to land at the right angle.
Quick tips
  • Build speed with a few pumps before you try your first flip.
  • On a big jump, one tap in the air is usually a full flip. Count, then stop tapping.
  • When in doubt, skip the flip: distance points still add up.
  • The game gets harder as you go, with tighter pump timing and landing requirements.
Behind the scenes
I built Pump-Send with a team of Claude Code agents: one designed the game, one wrote the tests (113 of them!), one built it, one reviewed the code, and one judged prototypes to help me choose among my four original ideas. The judging turned out to be less useful than I expected, since it was pretty obvious after playing which idea would pan out best. The most interesting lesson for me was about using bots to tune the physics.
Lesson one: the bot was too good
The first bot was simply too good at the game. It predicted where each jump would land and planned its flips accordingly, scoring hundreds of points. Then I took the wheel (err, handlebars) and crashed in seconds.
So I had the agents build a bot that plays like a person. It makes the same decisions, but each tap arrives 100 ± 60 ms late, it waits to see what one tap did before deciding again, and it taps at most 10 times per second. That bot survived a median of 8 seconds and crashed in 17 of 30 rides.
Here's what it uncovered and what I changed:
  • A slightly late tap on a lip turned into a backflip spin on a tiny hop. Now you only leave the ground when you tap.
  • Riders landed on the next uphill 55–68% of the time. Now a lip glows only if a pop at your current speed will land on a downhill.
  • A flat dead zone at every crest turned late taps into scrubs. It's gone.
  • Everything runs in gentle slow motion (0.8×), so every timing window lasts 25% longer.
  • In most flips, the rider's head went through the ground. Now the rider spins around the hips.
After those fixes, the human-like bot survived a median of 53 seconds with 0 of 30 crashes. "The human-like bot survives at least 30 seconds" is now a permanent test for every new build.
Lesson two: bad players on purpose
Bots that play badly are just as useful. Every build is tested against `idle` (never taps), `spam` (taps every frame), and `masher` (8 taps per second). Each one has to die within 15 seconds or score under a fifth of what the scripted bot scores, so you can't win by ignoring or mashing the button. A `random` tapper rounds out the lineup.
Results for the current build, 20 rides of up to 60 seconds each (a different set of rides from the 30 above, so the human-like numbers differ a little):
+------------+----------------------------+-----------------+--------------+--------------+
| Bot | How it plays | Median survival | Median score | Median flips |
+------------+----------------------------+-----------------+--------------+--------------+
| idle | Never taps | 12.8 s | 40 | 0 |
| spam | Taps every frame | 2.2 s | 1 | 0 |
| masher | 8 taps per second | 2.3 s | 9 | 0 |
| random | Random taps | 6.2 s | 30 | 0 |
| human-like | Smart taps, 100 ms late | 46.3 s | 801 | 8.5 |
| scripted | Smart taps, perfect timing | 51.2 s | 1,391 | 14.5 |
+------------+----------------------------+-----------------+--------------+--------------+
Your turn
What's your high score? Did you beat the human-like bot? I'd love to hear if anyone lands three flips in one jump.
The MATLAB One-Button Game Mini Hack runs from November 2nd through November 29th, 2026! The challenge is to make a game that can be played with one button. I thought this would be a good opportunity to share some of the graphics and programming ideas behind Arcade, a collection of 15 games I developed in MATLAB using Claude Code and the MATLAB MCP Server.
Before the 2024 MATLAB Shorts Mini Hack, Tim Marston published his walkthrough of Little Nemo's airship. I learned a lot from Tim's article, and it inspired me to enter the contest. I hope that sharing some of what goes into these games might encourage a few more people to give it a try this time around as well.
You can download Arcade from File Exchange or GitHub, add the project folder to the MATLAB search path, and run:
Arcade();
You can also download the two smaller examples below and experiment with them individually.
Creating and updating graphics
Arcade sports neon-styled graphics. This is accomplished by layering ordinary MATLAB graphics against a dark background. The bright colors used come from the shared GameBase palette, which enables the different games to keep a consistent appearance and accounts for quite a bit of that look.
In Pong, the ball is made by drawing a colored dot, a semitransparent scatter marker and a smaller white dot at the same position. The ball and trail change from cyan through green, gold and red as the ball speeds up, while the center stays white. Against the dark background, these simple shapes give us the bright center and surrounding glow.
To experiment with the ball, download arcadeBallDemo.m, add its folder to the MATLAB path, and run:
arcadeBallDemo();
The example creates the three layers with line and scatter, using the playfield axes, ax:
worldSize = [854, 480];
position = worldSize / 2;
cyan = [0, 0.92, 1];
radiusParameter = max(3, round(min(worldSize) * 0.035));
baseCoreSize = 2 * radiusParameter;
baseDotSize = 5 * radiusParameter;
baseGlowArea = pi * (1.25 * radiusParameter)^2;
coloredDot = line(ax, NaN, NaN, ...
Marker=".", MarkerSize=baseDotSize, LineStyle="none", Color=[cyan, 0.15]);
glow = scatter(ax, position(1), position(2), baseGlowArea, cyan, "filled", ...
MarkerFaceAlpha=0.4);
core = line(ax, position(1), position(2), ...
Marker=".", MarkerSize=baseCoreSize, LineStyle="none", Color=[1, 1, 1, 1]);
ballLayers = {coloredDot, glow, core};
Here's how the ball looks as each layer is added:
Setting NextPlot="add" preserves the existing graphics as we add the layers. Specifying the axes limits by using XLim and YLim keeps the view in place as the ball moves.
Each drawing call returns a handle to its object. With those handles stored in ballLayers, moving the ball involves assigning its new position to the existing objects:
for layerIndex = 1:numel(ballLayers)
ballLayers{layerIndex}.XData = position(1);
ballLayers{layerIndex}.YData = position(2);
end
drawnow
MathWorks' graphics-performance guidance recommends retaining graphics and updating the properties that change. Pong also checks whether its scores have changed before assigning new score text. Its trail uses preallocated position buffers, wrapping around as new positions arrive, and two retained lines display that history as a broad dim stroke with a narrower bright stroke.
Handling inputs using callback functions
The ball example responds to mouse presses through a callback function. After creating the scene, it assigns a function handle to the figure's WindowButtonDownFcn callback property:
fig.WindowButtonDownFcn = @reverseVerticalDirection;
MATLAB executes the callback function when a mouse button is pressed in the window. In the example, it is nested inside the local animateBall function:
function reverseVerticalDirection(~, ~)
velocity(2) = -velocity(2);
end
The two arguments supplied by MATLAB are the source object and event data. The ~ placeholders indicate that this function doesn't use them. As a nested function, it can access velocity in the enclosing function. Each mouse press reverses the shared vertical velocity, which the next loop iteration uses to move the ball.
Arcade routes input through methods on the launcher. Its figure's KeyPressFcn callback property is assigned as follows, where obj is the launcher instance:
obj.Fig.KeyPressFcn = @(~, event) obj.onKeyPress(event);
The anonymous function forwards the event data to onKeyPress, where its Key property identifies the key. The launcher handles its own controls and passes appropriate input to the active game. Button release and scrolling have their own callback functions.
Mouse movement can control a paddle or aim a shot. To track the pointer as it moves within the figure, Arcade assigns a callback function to WindowButtonMotionFcn:
obj.Fig.WindowButtonMotionFcn = @(~, ~) obj.onMouseMove();
Here's the mouse-position tracking part of the launcher's method:
function onMouseMove(obj)
pointer = obj.Ax.CurrentPoint;
obj.MousePos = pointer(1, 1:2);
end
The axes' CurrentPoint gives the pointer position in game coordinates. MATLAB accounts for the axes' position and size, so the same code works when the playfield is resized or surrounded by margins.
On each game update, the launcher passes the stored position to the active game's onUpdate method. In Pong, obj now refers to the game instance. Once the incoming pos is available, its vertical coordinate determines the player's paddle position:
yLimits = obj.DisplayRange.Y;
if ~any(isnan(pos))
obj.PlayerPaddleY = max(yLimits(1) + obj.PaddleHalfH, ...
min(yLimits(2) - obj.PaddleHalfH, pos(2)));
end
The min and max calls keep the paddle's center at least half a paddle height from either edge. Moving the pointer into a margin therefore leaves the whole paddle inside the playfield. The graphics update then applies that position to the existing paddle patches.
The animation loop's drawnow displays pending graphics changes and processes pending callbacks. Keeping callback functions short allows control to return to the loop promptly.
Animating visual effects
When the orb is launched in Flick It!, a call to spawnHitEffect creates a ring and rays that expand and fade. The launcher advances these shared effects alongside the active game.
In arcadeEffectsDemo.m, each click creates a burst at the mouse position, allowing several bursts to expand and fade together. To try it out, download the function, add its folder to the MATLAB path, and run:
arcadeEffectsDemo();
The nested callback function combines mouse-press handling with the mouse-position tracking introduced earlier. Here's the part that creates and stores a new burst:
fig.WindowButtonDownFcn = @createManualImpact;
function createManualImpact(~, ~)
point = ax.CurrentPoint;
position = point(1, 1:2);
if any(~isfinite(position)) || any(position < 0 | position > worldSize)
return
end
[newRing, newRays] = createEffect(position);
activeEffects(end+1) = struct('Ring', newRing, 'Rays', newRays, ...
'Center', position, 'AgeSeconds', 0, 'LastTime', toc(animationClock));
end
The bounds check ignores clicks outside the playfield. createEffect creates the lines, and activeEffects stores their handles, center and timing state. The animation loop updates each burst independently, allowing further clicks while earlier bursts are still expanding.
We can drive the geometry, transparency and line width from a common progress value: 0 when the burst begins and 1 at the end of its lifetime. Inside the function, the ring is updated as follows:
progress = ageSeconds / lifetimeSeconds;
eased = 1 - (1-progress)^2;
opacity = max(0, 1-1.5*eased);
ringRadius = radiusParameter * (1+2.5*eased);
ring.XData = center(1) + ringRadius*ringCos;
ring.YData = center(2) + ringRadius*ringSin;
ring.Color = [cyan, 0.8 * opacity];
ring.LineWidth = max(0.5, 3*(1-eased));
The ease-out expression starts the expansion quickly and slows it down toward the end. The burst fades out before its nominal lifetime finishes. ringCos and ringSin hold the circle's fixed directions, calculated before the loop. Arcade controls the transparency of these lines by assigning an RGBA value to Color, with alpha as the fourth component, ranging from 0 (clear) to 1 (fully opaque).
The row vectors rayCos and raySin contain the eight ray directions. Each ray joins the inner and outer radii, which increase as the burst expands:
innerRadius = radiusParameter * (0.5+1.5*eased);
outerRadius = radiusParameter * (1+3*eased);
for rayIndex = 1:numel(rays)
rays(rayIndex).XData = center(1) + [innerRadius, outerRadius]*rayCos(rayIndex);
rays(rayIndex).YData = center(2) + [innerRadius, outerRadius]*raySin(rayIndex);
rays(rayIndex).Color = [cyan, 0.6 * opacity];
rays(rayIndex).LineWidth = max(0.5, 2*(1-eased));
end
The lines are created when the burst begins, updated throughout its lifetime and deleted when it expires.
Display scaling: Keeping the scene in proportion
Try making the ball example's window wider while keeping its height the same. Space appears beside the playfield, and the ball still travels between the same boundaries. The example's reference window is 854 by 480 pixels, matching worldSize numerically. Resizing scales the playfield on the screen while the game's coordinate system remains unchanged.
The figure and axes use Units="pixels". Taking the min of the width and height ratios relative to the reference window gives the largest scale factor that fits in both directions. Applying that same scale to both dimensions preserves the playfield's 16:9 aspect ratio.
For a 1200-by-480 window, the height limits the scale to 1. The fitted area remains 854 by 480, leaving (1200-854)/2 = 173 pixels on each side. In a 1280-by-720 window, the playfield grows to approximately 1.5 times its reference width and height. Here are four window sizes at a common page scale:
MATLAB already scales graphics defined in data coordinates as the axes resize. For example, a paddle drawn with patch grows with the axes. In the example, marker sizes, font sizes and line widths use points, while scatter sizes use points squared.
To make a circle twice as wide, we need four times its area. Doubling SizeData multiplies its width by only √2:
For resizing, we assign fitScene to the figure's SizeChangedFcn property:
fig.SizeChangedFcn = @fitScene;
The nested fitScene callback function combines the axes calculation with these size updates:
function fitScene(~, ~)
if ~isgraphics(ax)
return
end
availableSize = max(fig.Position(3:4), [1, 1]);
displayScale = min(availableSize ./ worldSize);
fittedSize = worldSize * displayScale;
ax.Position = [(availableSize - fittedSize)/2 + 1, fittedSize];
% Always derive sizes from their reference values
core.MarkerSize = baseCoreSize * displayScale;
coloredDot.MarkerSize = baseDotSize * displayScale;
glow.SizeData = baseGlowArea * displayScale^2;
instruction.FontSize = baseFontSize * displayScale;
playfield.LineWidth = 0.5 * displayScale;
end
The first two entries of ax.Position set the left and bottom position, splitting the unused space equally between opposite sides. The last two give the fitted width and height. The axes limits remain unchanged, so movement and collision calculations keep using the same game coordinates.
At scale 1.5, the marker sizes are multiplied by 1.5 and the scatter area by 2.25. The updated sizes are calculated from their stored reference values. A 34-point reference marker becomes 51 points at scale 1.5, then 68 points at scale 2.
You can also use Units="normalized" to size and position the axes relative to the figure. Arcade's axes setup uses [0 0 1 1] to allocate the full available area:
obj.Ax = axes(obj.Fig, "Units", "normalized", "Position", [0 0 1 1]);
obj.Ax.XLim = obj.DisplayRange.X;
obj.Ax.YLim = obj.DisplayRange.Y;
Arcade's resize method obtains the available dimensions in pixels with getpixelposition and calculates the same display scale. It uses pbaspect to preserve the playfield's aspect ratio. MATLAB fits the playfield within the available space, leaving margins where needed:
axPx = getpixelposition(obj.Ax);
newPs = min(axPx(3) / 854, axPx(4) / 480);
gameAR = diff(obj.DisplayRange.X) / diff(obj.DisplayRange.Y);
pbaspect(obj.Ax, [gameAR 1 1]);
For Arcade, I also wanted markers, text and line widths to grow proportionally with the playfield. MATLAB also supports automatic resizing for text through FontUnits="normalized", which expresses font size as a fraction of the axes plot box height. This works well for text in a playfield with a fixed aspect ratio. Arcade's shared resize helper modifies current property values, using the ratio of the new scale to the previous scale; for example, moving from 51 to 68 points requires multiplying by 2/1.5. The function applies a common display scale to text, markers and line widths, in order to preserve their relative sizes as the window is resized.
FPS scaling: Let elapsed time determine the motion
Suppose that the ball moves 5 game units on each update. At 60 updates per second it travels 300 units in a second, but at 30 it travels only 150. Calculating the state, drawing and processing input all contribute to the time between updates, so we need to measure that interval to maintain a consistent speed.
Gianluca Carnielli discusses the relationship between time and animation frames in Animating Science: Creating Time-Sensitive Animations with MATLAB. In his example, he explains how to create a frame-based animation from time-based coordinates, by choosing the video’s duration and frame rate, calculating the simulation timestep between two consecutive frames, and resampling the coordinates using retime.
In a live game, we process player actions and calculate each new state using the time elapsed since the previous update. Arcade expresses many motion parameters per reference interval, using 60 frames per second (FPS) as its reference frequency. An elapsed time of 1/60 seconds represents 1 reference interval, while 1/30 seconds represents 2. In the example, we define the velocity and reference frequency before the loop and start the clock with tic once the initial scene is displayed:
velocity = [5, 3]; % Game units per reference interval
referenceFPS = 60; % Reference intervals per second
animationClock = tic;
previousTime = 0;
Each iteration measures the time with toc and advances the position:
currentTime = toc(animationClock);
dtSeconds = min(currentTime - previousTime, 0.1);
previousTime = currentTime;
dtScale = dtSeconds * referenceFPS;
position = position + velocity * dtScale;
Here dtSeconds is in seconds, and dtScale counts reference intervals. A horizontal velocity of 5 now gives a movement of 10 game units over 1/30 second. Taking 30 steps of 10 units covers the same 300 units as 60 steps of 5 units. The samples are farther apart, so the motion can look less smooth:
The min(..., 0.1) caps a single represented interval at 0.1 seconds (10 FPS) to limit jumps after a stall. If an update arrives after 0.5 seconds, the code advances only 0.1 seconds, and the remaining 0.4 seconds are discarded.
The reference frequency is 60 FPS; the actual update rate depends on execution. We could also use game units per second and write position = position + velocityPerSecond * dtSeconds.
Arcade schedules its updates with a timer. The callback function assigned to its TimerFcn property calls the launcher's frame-update method:
obj.RenderTimer = timer(ExecutionMode="fixedSpacing", ...
Period=0.02, TimerFcn=@(~, ~) obj.onFrame());
start(obj.RenderTimer)
With fixedSpacing, the period begins after the preceding callback function finishes. The function's own work therefore contributes to the interval between updates. The launcher measures the full interval and passes the resulting time scale to the active game.
The ball example uses a loop with pause. Try increasing that pause:
arcadeBallDemo(Inf, 0.04);
Its measured interval includes both the pause and the surrounding work. Both Arcade and the example use the full drawnow command. The limitrate option can reduce rendering work by limiting display updates to 20 per second; its suitability depends on the animation's required display rate.
Flick It! multiplies its velocity by friction^dtScale, where friction is the fraction of velocity retained over one reference interval. Over 2 intervals, the multiplier is the square of that factor, equivalent to applying it once in each interval. This makes the velocity decay follow the elapsed time.
For accelerating objects, the numerical approximation can still change with step size, and collisions need to account for movement between samples. Pong checks when the ball crosses the paddle's front face and interpolates its vertical position there, allowing a contact to be detected between two displayed positions.
Organizing games using classes
Arcade's registry stores a constructor function handle for each game. When a game is selected, the launcher calls the corresponding constructor to instantiate the game class. Here's an adaptation of the launch sequence, with Pong selected explicitly and obj referring to the launcher:
createGame = @games.Pong;
game = createGame();
game.FontScale = obj.FontScale;
game.init(obj.Ax, obj.DisplayRange)
game.beginGame()
obj.ActiveGame = game;
Each game subclasses engine.GameBase. The shared init method sets up the session and calls the game's onInit, which creates its state and graphics. Assigning FontScale beforehand lets newly created markers and text use the current display size. The launcher stops its render timer before creating the game and restarts it after the initialized game is assigned to ActiveGame. This prevents timer callbacks from running while the game’s state and graphics are being created.
GameBase declares four abstract methods that each game class must implement:
The launcher calls onUpdate on its active game, and MATLAB dispatches that call to the selected subclass. Pong implements paddle and ball movement; Flappy Bird advances its bird and pipes. All games inherit methods for scoring, combo handling and visual effects from GameBase. Window management, menus, timing and common services stay with the launcher.
As a handle class, GameBase lets the launcher keep a reference to the same game instance while its state changes. Position arrays, velocities, scores and graphics handles belong to that instance. Property access attributes allow the host to read the common score while leaving changes under the control of the game classes.
Flappy Bird creates its pipe patches in onInit, then reuses them throughout the game. Their handles and state are stored in private properties of the game instance. Its onUpdate method moves the active pipes across the playfield. Spawning a pipe selects an available pair, assigns its position and gap, and adds it to the active list. Once it leaves the playfield, the pair is hidden and its slot becomes available again:
In the default gravity-and-flap mode, the onKeyPress input handler sets a pending flap flag. The next update applies the upward impulse and clears the flag.
When the window closes, Arcade stops and deletes its timer, calls the active game's onCleanup method and clears its effects before deleting the figure. The smaller examples use MATLAB's onCleanup object to delete their own figure when the function exits. The requestClose callback function assigned to the figure’s CloseRequestFcn property sets a stop flag, letting the current update finish before cleanup removes the graphics.
Recent work on MATLAB graphics and performance
The MathWorks graphics team has also been improving the underlying rendering system. Maddie Moon's article about WebGL describes the R2025a transition, which brought the renderer already used by uifigure to regular MATLAB figures, along with work on stability, consistency and smoothing. Will Jackson also explains related changes to built-in panning, zooming and rotation in his article on the new interactions system.
R2025b focused on quality and stability improvements, building on the changes introduced in R2025a. It also improved performance when capturing successive frames in a loop with getframe, the function used by Arcade’s recording helper to capture and store frames for GIFs and videos.
R2026a included memory and stability improvements for graphics such as scatter and surface plots, described in Adam Danz's overview and the release notes. In addition, Jessa Karlberg describes the redesigned axes toolbar in R2026a, which allows seamless hovering without the animation slowdown issues caused by the toolbar’s continuous mouse tracking in earlier releases, eliminating the need to remove the toolbar as a workaround.
R2026b improved responsiveness for large data visualizations, with plot and plot3 being able to display lines and markers incrementally and shortening the wait time for the first visible results. MathWorks also reports automatic selection of a dedicated rendering GPU on supported Windows and Linux systems with multiple GPUs. These changes are documented in the release notes and the GPU-selection update.
You can inspect your session's default renderer and device with rendererinfo:
info = rendererinfo;
info.GraphicsRenderer
info.RendererDevice
Mike Croucher also describes a new object-management system for R2026b, which can speed up object-oriented MATLAB code by reducing the cost of creating and deleting objects, as well as accessing their properties. The system is currently available as a beta and can be enabled through a package on File Exchange. Its documentation explains the changes to object lifecycle and compatibility.
Developing MATLAB code with AI
Arcade was developed with Claude Code through the MATLAB MCP Server, and the contest welcomes AI assistance as an option.
When working with an AI assistant, consider sharing your project's requirements and any existing code it can build on. Arcade's developer guide, for example, describes the shared game interface and explains how to add a new game. For your own game, you could begin with the main interaction and add features gradually, playing and improving it as you go. When something goes wrong, describe what happened and how to reproduce the problem, such as a ball passing through a wall at higher speeds.
The MATLAB Agentic Toolkit provides workflows for code review, analysis and testing. Its setup guide covers connecting MATLAB to AI applications including Claude Code, GitHub Copilot, OpenAI Codex, Gemini CLI and Amp, and installing MATLAB-specific skills. Through the MCP server, the AI assistant can run MATLAB code and tests, inspect the results, and use them to help diagnose problems and revise the code.
Several implementation choices in Arcade align with the MATLAB Coding Guidelines and performance rules: using a handle class for an object whose identity persists, restricting access to its state, retaining graphics, preallocating known-size arrays and cleaning up timers. The two downloadable examples presented above were checked with MATLAB Code Analyzer and tested for resizing, input, handle reuse and cleanup.
I hope that this article gives you something to experiment with, and perhaps a starting point for the One-Button Game Mini Hack. I look forward to seeing what you make!
pippis
pippis
Last activity on 9 Nov 2026 at 12:43

Hi there,
Is there a change in the json response when I do http write to a channel ?
I send:
POST /update.json HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: api.thingspeak.com
Connection: close
Content-Length: 102
apikey=xxxxxxxxxxxx&field1=-4.803&field3=18&field4=91&field5=109&field6=56&field7=0&field8=7
and I don't receive the normal response
I get sth like:
'unsafe-eval'; style-src 'self' https: 'unsafe-inline'; connect-src 'self' https: https://*.mathworks.com https://*.thingspeak.com https://*.omtrdc.net https://*.demdex.net; frame-src 'self' https: https://*.mathworks.com
Once you've uploaded your one-button game, it doesn't seem like there's a way to edit/update it. That's led me to fall into a couple of traps. One has a simple solution, so I'll share it. The other has me still scratching my head, so I'll open it up for discussion.
  • Restarting. While you're developing and testing in MATLAB, it's easy enough to close the window and rerun your game. But the process is clunkier in the online gallery, so make sure your game includes a clean way to restart. If you're using an agent, it's easy enough to instruct it to modify the spacebar/click callback when the game ends so that it now calls the start/initialization function. This is a nice job for an agent (standard coding pattern it will probably do right, while I'll mess up and get annoyed).
  • Timing. I can't figure out how to translate frame rate on my slow desktop MATLAB to the online running framework. Things that seem very sedate while testing suddenly get very exciting when I upload. Anyone know a good way to emulate just how zippy the real game will be?
My entry for the one-button game mini hack Lunar Lander - Tap to Burn, is a one-button powered descent. You start 500 m above the surface, falling at 10 m/s, with 200 kg of propellant. One tap turns on the engine, and the next tap stops it.
How scoring works
  • Crash: landing at 3 m/s or faster scores 0.
  • Abort: rising above 750 m scores 0.
  • Safe landing: below 3 m/s scores 10 points per kilogram of fuel remaining.
  • Perfect touchdown: below 1 m/s doubles the reward to 20 points per kilogram of fuel remaining.
Physics in the game
  • Fuel mass decreases as propellant is burned.
  • Gravity continuously accelerates the lander downward.
  • The engine does not reach full thrust instantly, so timing matters.
  • The display turning red indicates when a full thrust landing burn becomes possible.
Display readouts
  • ALT [m]: Height above the surface.
  • V/S [m/s]: Vertical speed.
  • FUEL [kg]: Remaining propellant.
  • dV [m/s]: remaining velocity change the fuel can provide, from the rocket equation Δv=Isp·g₀·ln(m/m_dry).
  • T/W: Thrust to weight ratio.
My best so far is 1300. Has anyone managed a sub 1m/s landing?
Cephas
Cephas
Last activity on 3 Nov 2026 at 14:28

The day and night colours depend on how far the fox has run. Instead of recolouring everything on every frame, I turn the cycle into a small number of steps and only recolour when the step changes:
nn=.5-.5*cos(2*pi*s.dist/3200);
q=round(nn*40);
if ~isequal(s.ax.UserData,q)
s.ax.UserData=q;
% recolour the background, ground, text and obstacles here
end
The current step is stored in the axes UserData, so the check is cheap. Text works the same way. I only set a text string when the new string is different from the old one:
if ~strcmp(s.sct.String,t), s.sct.String=t; end
Setting properties that have not changed still costs time in every frame, so skipping them helps a lot in a game that redraws many times per second.
Check against your code: the first block is simplified from your renderFrame. Your real code also adds +100*(s.st==2) to q so the colours change on the game-over screen, and I left that out to keep the tip simple. The strcmp line is from your score text. Both matc
The day and night colours depend on how far the fox has run. Instead of recolouring everything on every frame, I turn the cycle into a small number of steps and only recolour when the step changes:
nn=.5-.5*cos(2*pi*s.dist/3200);
q=round(nn*40);
if ~isequal(s.ax.UserData,q)
s.ax.UserData=q;
% recolour the background, ground, text and obstacles here
end
The current step is stored in the axes UserData, so the check is cheap. Text works the same way. I only set a text string when the new string is different from the old one:
if ~strcmp(s.sct.String,t), s.sct.String=t; end
Setting properties that have not changed still costs time in every frame, so skipping them helps a lot in a game that redraws many times per second.
All the graphics objects (patches, lines and text) are created one time, when the game starts. After that, the render function only changes their data with set:
set(s.hd,'XData',s.DS{k}+DX,'YData',s.DT{k}+G+s.dy)
Creating new objects on every frame is a common reason MATLAB games feel slow. Updating objects that already exist is much cheaper. To hide an object, I set its data to NaN instead of deleting it:
set(s.ho(m),'XData',NaN,'YData',NaN)
Disclosure: the game code was written with Claude (an AI assistant). I tested the game in MATLAB and checked the details myself.
For the One-button Game Mini Hack, the code limit is 10,000 characters. I made a game called Ember Fox, where a fox runs and a stone guardian chases it, and you have one button. These are the techniques in the code that I found most useful.
Tip 1: Draw pixel-art sprites from text
Each sprite is stored as a grid of characters, where # is a filled pixel and . is empty:
A=['######';'#.##.#';'######';'.####.'];
[X,Y]=spr(A,0.6);
A helper function, spr, turns the grid into rectangles. For each row it uses diff to find where every run of # starts and ends, and each run becomes one rectangle instead of one rectangle per pixel. This keeps the sprite data small and the number of graphics objects low. All the sprites are built once, when the game starts.
If you are as excited as me to play these games but somehow something is stopping you from doing just that, you are at the right place. Follow the steps below and you will be playing in no time (maybe some time depending on your browser, network speed, etc.):
  1. Click on the game you like the look of (let's say, something that is transformative).
  2. This will open up its details page with the game code, preview video and the "Play Game" button. Click the video or the button.
  3. This will open up "MATLAB Drive" in a new browser window with a preview screenshot of a half-played game and a "Run this App" button. Click this button and nothing will seem to happen at first. And nothing will happen at any other subsequent tries. But don't worry, that's why you're here.
  4. Click on the "entry-×××××" blue hyperlink just above the preview area (underlined in red above). This will take you to a list of two files but hide the crucial blue button. Click on the "Entry×××××.mlapp" file to resurface the blue button.
  5. With expectant hands, click the "Run this App" button. This time it will reload the "MATLAB Drive" and put it in the mode to run your chosen game.
  6. Finally, you are where you expected to be in the point 3! Enjoy your game.🥳