Padel Heaven, deployed at padel.jekardah.com, is an arcade doubles padel match — the player and a computer partner against two computer opponents — on a court styled after Bali at golden hour. It ships as three static files totalling 19,937 bytes, 7,903 when gzipped: an HTML shell, a stylesheet, and a 13-kilobyte script that includes no library, loads no image or font, stores nothing and makes no request of its own. Everything that moves is drawn on one 2D canvas, every frame. Depth comes from a projection of a few lines — a linear depth factor over a fixed ground foreshortening, with a camera toggle that is a 9.8° yaw of the same function — and occlusion from a painter's algorithm ordered by layer rather than by object. The world is measured in metres: a 10 × 20 m court, service lines at 7 m, a net tested at 0.94 m, and rebounds with a restitution of 0.8 off the glass and 0.76 off the floor. Shots are not simulated forward to discover where they land; they are solved backward. Each hit chooses a landing point 4.5–7.5 m beyond the net and a flight time of 1.05 s, or 1.65 s for a lob, and the launch velocity follows in closed form from the projectile equation, so every ball the game generates is aimed inside the court. Scoring is padel's golden point in a first-to-three-games set. The three computer players share one rule: the receiving-side player nearest the ball chases a point a tenth of a second ahead of it. To test the code rather than only describe it, the deployed script was run unmodified in a headless harness for 13,409 points. The replay confirms that the out and glass-before-bounce branches never fire; shows that net faults are more than forty times as frequent at 30 frames per second as at 60, and that the explicit integrator lands a shot 0.19 m beyond its target at 60 Hz and 0.57 m at 30 Hz; and exposes a blind spot in the opponent rule. Because the receiving side is inferred from the sign of the ball's velocity, which flips at the back glass, the computer players played none of 2,424 back-wall rebounds. Choosing the receiver as the side that did not strike last lets them strike 6% of rebounds in the same experiment.
Padel is played in doubles on an enclosed court of 10 by 20 metres, smaller than a tennis court, and the walls are part of the game: a ball that bounces and then strikes the glass is still in play. That makes it an attractive subject for a small arcade game and an awkward one, because the interesting part of the sport — the rebound — is exactly the part that a simple game model is most likely to get wrong.
Padel Heaven is such a game. One person controls the player marked in mint; a computer partner, Maya, plays beside them against Luna and Sofia. The first screen offers “Your court. Your moment.” and a single button, Masuk lapangan; the match is a short set of three games with golden-point scoring; the scoreboard, the key hints and the running commentary are in Indonesian, and an on-screen credit says the setting was inspired by Padel Heaven Bali. The whole application is three files served from a static host. This paper describes the decisions inside those files and then checks them: the deployed script was run unmodified in a headless harness, with the rendering stubbed out and the simulation driven directly, so that claims about which rules can fire, how the physics behaves at different frame rates, and what the computer players can and cannot do rest on measurement rather than on reading alone.
| File | Bytes | Gzipped | Contents |
|---|---|---|---|
index.html | 2,372 | 1,101 | The canvas; header with brand, sound and pause buttons; the DOM scoreboard; the toast; the intro and result panels; key hints and camera toggle; a hidden touch pad. |
style.css | 4,480 | 1,578 | Overlay styling over a full-viewport canvas, and one breakpoint at 800 px that hides decorative text and reveals the touch pad. |
game.js | 13,085 | 5,224 | Projection, drawing, physics, rules, AI, input and sound: 446 lines when re-printed, about twenty named functions. |
| Total | 19,937 | 7,903 |
The absences are the design. There is no framework and no third-party code; every identifier in the script belongs to the game. There are no images and no web fonts — headlines are set in Georgia and everything else in Arial — so the page makes two subresource requests and then none of its own. Nothing is written to local storage or IndexedDB: a match lives in memory and ends with the tab. The script is delivered minified but not mangled, so the names in this paper are the names in production.
The subdomain is a CNAME to a managed static-site host behind Cloudflare, and its only certificate in the public Certificate Transparency logs was issued on 9 September 2026, which dates the deployment. Responses carry Cache-Control: public, max-age=0, must-revalidate with an ETag, so a returning visitor revalidates rather than re-downloads. The host answers every path with the page itself: /robots.txt, /sitemap.xml and /favicon.ico all return 200 text/html. The edge network also appends its own bot-detection snippet to some responses; it is the only script on the page that the game did not bring.
The simulation has one unit, the metre, and one coordinate system: x across the court, z along it with the net at zero and the player's half positive, and y up. Drawing, physics and AI all use it, which is why the numbers in the code can be compared directly with the sport's own.
| Quantity | In the game | Regulation padel |
|---|---|---|
| Court | 10 × 20 m (x ±5, z ±10) | 10 × 20 m |
| Service lines | 7 m from the net | 6.95 m from the net |
| Net | Drawn 0.9 m; a crossing below 0.94 m is a fault | 0.88 m at the centre, 0.92 m at the posts |
| Walls | 3 m glass on both sides and at the far end; the near end drawn as 2.6 m translucent posts | Glass back walls with metal mesh above; glass and mesh side panels |
| Ball | Radius 0.115 m; floor contact when its centre falls below 0.12 m | Tennis-type ball, 6.35–6.77 cm diameter |
| Gravity | 9.8 m/s² | — |
| Rebound | Floor returns 76% of vertical speed; glass returns 80% of the normal component | — |
| Players | About 1.9 m tall; the human moves at 5.2 m/s, computer players at 4.2 m/s | — |
The ball is drawn at roughly three and a half times its real size so that it reads on a phone. Around the court the scene is furnished at the same scale: a 16 × 26 m sand apron with a one-metre tile grid, four 6 m floodlight poles, four planters with palms, a clubhouse ten metres wide with five dark windows, and a back row of nineteen palms between five and eight metres high, each swaying on its own phase.
The canvas is sized to the viewport multiplied by the device pixel ratio and then scaled back, so all drawing happens in CSS pixels. A world metre becomes scale = min(W/31, H/26) pixels, which fits a 31 by 26 metre view in any window. Every point then passes through one function:
depth = 1 + 0.024·z screenX = 0.51·W + x·scale·depth screenY = 0.48·H + 0.65·z·scale·depth − y·scale·depth
It is not a perspective projection, and it does not need to be. The depth factor grows linearly toward the viewer, so the near baseline is drawn at 1.24 times the base scale and the far one at 0.76, a ratio of 1.63; the factor 0.65 compresses the ground into an elevated broadcast view; and height is subtracted in screen space at the same local scale. The camera toggle, labelled Broadcast and Sinematik, does not move a camera: when it is on, x and z are rotated by a matrix with cosine 0.985 and sine 0.17 before projection, a yaw of 9.8° applied to the whole world.
There is no depth buffer, so occlusion is a painter's algorithm, and the order is chosen by layer: sky gradient, sun glow, ground, palms, clubhouse, apron and grid, court and lines, side and far glass, floodlights and planters, then the players on the far half sorted by depth, the net, the players on the near half sorted by depth, the ball and its shadow, the translucent near glass, and a vignette. Sorting within each half and fixing the net between the halves is enough for bodies, which never cross the net.
The four players are procedural drawings rebuilt every frame from lines and polygons scaled by their local depth: a ground shadow, legs that stride when the player is moving, shoes, a shirt and a flared skirt with five faint pleats, a swinging racket arm, a head, a hair cap, a ponytail that bobs, a headband and a racket. The racket head is an ellipse tilted 0.3 radians with a three-by-three grid of holes, because a padel racket is perforated and a tennis racket is strung. The controlled player's racket rim is mint and a mint ring and pointer mark them on the court. Instrumenting the live page shows the cost of one frame: 562 paths, 312 fills, 258 strokes, 1,019 line segments, 36 arcs for the racket holes, four text draws and three freshly created gradients, in about 0.3 ms of script time on an Apple-silicon laptop at a 1024 × 768 viewport and a pixel ratio of two.
A physics game usually launches the ball with some velocity and lets the integrator decide where it comes down. Padel Heaven inverts that. When a player strikes the ball, the game first decides the outcome — a landing point and a flight time — and then computes the one velocity that produces it.
| Parameter | Rule |
|---|---|
| Depth | Uniformly 4.5–7.5 m beyond the net, on the opponents' side. |
| Width, human | −3.4 m while A or ← is held, +3.4 m while D or → is held, otherwise −0.5 × the player's own x, a mirrored cross-court ball. |
| Width, computer | Uniformly −3 to +3 m. |
| Width, serve | Cross-court to ±2.2 m, whatever keys are held. |
| Flight time | 1.05 s for a drive, 1.65 s for a lob (the L key, the LOB button, or 15% of computer strikes). |
| Contact height | Raised to at least 0.65 m, so a ball scooped near the floor leaves from a playable height. |
With start position (x0, y0, z0), target (xt, zt) and flight time T, the horizontal velocities are distances over time and the vertical one comes from requiring the ball to be 0.13 m above the floor — just above the 0.12 m contact plane — at time T under gravity:
vx = (xt − x0) / T vz = (zt − z0) / T vy = (0.13 − y0 + 4.9·T²) / T
The live page confirms it. A first serve from (−2.4, 0.9, 6) produced vx = 4.381, vy = 4.412 and vz = −10.031: the cross-court target at +2.2 m, a flight of 1.05 s, and a landing 4.53 m past the net. From a contact height of 0.9 m a drive peaks at 1.89 m and a lob at 3.86 m, which matters because a computer player only strikes a ball below 1.9 m and the human below 2.6 m.
Solving backward has two consequences. Every ball the game generates is aimed inside the court, because the width targets stay within ±3.4 m of a 10 m court and the depth targets stop 2.5 m short of the back glass, and horizontal motion is a straight line from the contact point to the target. And skill moves from aiming to positioning: a player chooses the side with the key they are already holding to run, and the game guarantees the ball goes in. The rule branches for a ball that flies out or strikes the glass before bouncing remain in the code; Section 9 shows that they never fired.
Points count 0, 15, 30, 40, and the first team to a fourth point wins the game. There is no advantage: at 40–40 the next point decides, which is padel's golden point. The first team to three games wins the match, without a two-game margin or a tie-break, and the scoreboard strip says so: set pendek · 3 game · golden point. The serve passes to the other team after every game.
| Event | Check | Point to | Commentary |
|---|---|---|---|
| Net | The ball changes sides below 0.94 m | The side that did not strike | bola mengenai net |
| Side glass before the floor | |x| > 4.9 m with no bounce since the strike or net crossing | The side that did not strike | kaca sebelum pantulan |
| Back glass before the floor | |z| > 9.9 m with no bounce | The side that did not strike | bola keluar |
| Second bounce | Two floor contacts since the strike or net crossing | The side opposite the bounce | dua pantulan |
| Glass after the floor | Either wall, after a bounce | No point: the ball rebounds at 80% | — |
Legal glass after a bounce is the rule that makes the game padel rather than tennis, and it is implemented faithfully. The serve is simplified. Only Anda and Luna serve, always from x = −2.4 m and six metres from the net, inside the service line; there is no bounce-and-underarm requirement and no service box, and a sixth of serves land beyond the service line where a regulation serve would be a fault. The human may walk anywhere on their half before serving, carrying the ball with them; Luna serves after a 1.5 s pause. A strike is accepted only while the ball is live, from the side that did not strike last, at least 0.25 s after the previous strike, within 1.6 m horizontally and no higher than 2.6 m.
The computer players are not three agents but one rule applied to three bodies. On every frame the game names the side the ball is travelling toward, finds the player on that side nearest the ball — the human included — and makes that player the target. A computer player that is the target, on the side that did not strike last, runs toward where the ball will be: 0.12 s ahead of it across the court and 0.13 s along it, clamped to its own half. Every other computer player walks back to a home spot 2.4 m from the centre line and 5 m from the net. Movement is a capped seek at 4.2 m/s, and a target within 1.25 m of a ball below 1.9 m strikes it.
| Human | Computer | |
|---|---|---|
| Speed | 5.2 m/s, normalised on diagonals | 4.2 m/s |
| Reach | 1.6 m | 1.25 m |
| Highest strike | 2.6 m | 1.9 m |
| Aim | Left, right or mirrored cross-court, by the key held | Uniform random width |
| Error | Whatever the person makes | None: a ball in reach is always returned |
The nearest-player rule also settles doubles etiquette without communication. When the ball comes toward the human's side and the human is nearer, Maya stays home and the ball is the human's to play; when Maya is nearer, she takes it. Because the computer never misses once it arrives, difficulty is purely a matter of whether it can arrive.
Keys are recorded by lower-cased name, so W A S D and the arrow keys drive the same four flags. Space strikes and L lobs on the initial key press only, so holding a key does not machine-gun swings; arrow keys and Space have their default scrolling suppressed; Escape toggles the pause. When the window loses focus every key flag is cleared and a running match pauses itself, which removes the classic stuck-key bug of browser games. Below 800 px a touch pad appears with a d-pad and PUKUL and LOB buttons; each button writes the same key flags on pointerdown, captures the pointer so that its pointerup arrives even if the finger slides off, and is hidden while a menu is showing.
Time is taken from requestAnimationFrame and each step is clamped to 35 ms, so a background tab or a stalled frame never teleports the ball; below about 29 frames per second the game runs in slow motion instead. Sound is off by default. The first press of the sound button creates the audio context inside a user gesture, and every event is then a 100 ms sine blip decaying from a gain of 0.05, pitched by meaning: 450 Hz for a drive, 620 Hz for a lob, 330 Hz for the floor, 280 Hz for glass, and 780 Hz or 240 Hz for a point won by the home or the away pair, so the outcome of a rally can be heard without looking. The scoreboard is real DOM text rather than canvas pixels, and the commentary line is a role="status" live region, so a screen reader announces serves, rallies and points as they happen.
The deployed game.js was loaded unmodified into a Node.js vm context with a stub document, a canvas context whose methods do nothing, and a deterministic replacement for Math.random. A short accessor appended after the script exposed its top-level state; the simulation was advanced by calling its own update(dt) at a fixed step; and point outcomes were read from the commentary line, which names the reason for every point except the last point of a match. Slot 0 was driven either by a scripted player given exactly the human's limits — 5.2 m/s, 1.6 m reach, 2.6 m ceiling, movement keys as aim, a lob one strike in ten — or left idle after serving, so that every rally was played by the computer. As a check on the harness, a 60-match run recorded 127,477 strikes by the scripted player and 382,452 by the computer players, with no two consecutive strikes by the same side.
| 60 Hz | 30 Hz | |
|---|---|---|
| Points played | 5,329 | 5,656 |
| Points with a named reason | 4,929 | 5,256 |
| Second bounce | 4,927 | 5,164 |
| Net | 2 (0.04%) | 92 (1.75%) |
| Side glass before the floor | 0 | 0 |
| Back glass before the floor | 0 | 0 |
| Matches won by the scripted side | 400 of 400 | 400 of 400 |
| Mean rally when a point was decided | 659 strikes | 472 strikes |
Three things follow. The two glass-before-floor branches never fired in 10,185 decided points, as Section 5 predicts. Net faults depend on the frame rate: the net is tested only at whole frames, against the ball's height after it has already crossed, and at 30 Hz a fault was more than forty times as likely as at 60 Hz. And a player that never misjudges a ball is never beaten by these opponents: rallies ran to hundreds of strikes, because the computer returns everything it reaches and points end only when a ball is placed beyond a defender's reach. Every such ball in this experiment struck the back glass before its second bounce, which raises the question of what the computer does with a ball coming back off the wall.
| Deployed: receiver is the side the ball moves toward | Changed: receiver is the side that did not strike last | |
|---|---|---|
| Back-glass rebounds | 2,424 | 2,552 |
| Rebounds struck by a computer player | 0 | 152 (6.0%) |
| Points decided, all by second bounce | 2,224 | 2,200 |
| Simulated seconds per point | 13.1 | 13.7 |
The deployed rule reads the receiving side from the sign of the ball's velocity along the court. The back glass reverses that velocity, so at the moment of the rebound the receiving side becomes the side that struck last, which may not strike again — and the defenders, no longer the target, walk home. In 2,424 rebounds no computer player struck the ball once. Choosing the receiver as the side that did not strike last, a change to one expression, let computer players strike 152 of 2,552 rebounds. The effect is real and modest: most balls that reach the back glass were out of reach before they got there.
| Frame interval | Drive, T = 1.05 s | Lob, T = 1.65 s |
|---|---|---|
| 6.9 ms (144 Hz) | +0.14 m | +0.07 m |
| 8.3 ms (120 Hz) | +0.19 m | +0.12 m |
| 16.7 ms (60 Hz) | +0.19 m | +0.24 m |
| 33.3 ms (30 Hz) | +0.57 m | +0.36 m |
| 35 ms (the clamp) | +0.40 m | +0.47 m |
The closed-form aim assumes continuous motion; the update moves the ball with its velocity from before gravity is applied, an explicit Euler step, which leaves it ½·g·T·Δt higher than the ideal arc at time T — 8.6 cm for a drive at 60 Hz — and the bounce is detected only at the first whole frame below the contact plane. Both push the landing later and longer, by amounts that depend on the frame interval and do not shrink monotonically with it.
The findings above are the principal limits: a computer side that never plays the back glass, net faults and landing points that depend on the frame rate, and opponents that never miss a ball they reach. The simulation also inherits the usual cost of a variable time step — the same inputs do not produce the same rally on a 60 Hz laptop and a 120 Hz phone — which a fixed-step accumulator would remove.
Several smaller departures from the sport and from correct drawing are visible in the code. A player may strike a ball before it crosses the net — up to a metre early for the human, who can stand 0.6 m from the net and reach 1.6 m — because the strike check does not look at which side the ball is on. Only Anda and Luna ever serve. The ball is painted after the net and every player regardless of depth, so a low ball behind the net appears in front of it. The solid centre line is drawn between the service line and the back glass, and only a faint line divides the service boxes, the reverse of a real court's markings. And the projection is quadratic in depth on the vertical axis while polygons are drawn with straight edges, so objects sampled mid-court do not lie on the lines drawn through them: at 1280 × 800, a point on the sideline at the net projects 8.9 px, or 0.29 m, outside the drawn sideline.
The render loop never idles. The full scene, with three gradients created per frame, is redrawn at the display's rate behind the intro panel, while paused and after the match, and the canvas backing store is not capped at any pixel ratio. On a 375 px phone the 31-metre view gives about 12 pixels per metre, so the players are between about 20 and 26 pixels tall, and the commentary still says tekan Spasi, press Space, on a device whose button reads PUKUL. The sound button's aria-label is fixed at Aktifkan suara, enable sound, and overrides the visible text, so assistive technology hears the same name whether sound is on or off. The script uses ES2021 syntax, so browsers released before mid-2020 reject the whole file. Finally, the page carries no description or social-preview metadata, and because the host answers every path with the page, crawlers asking for robots.txt and browsers asking for a favicon receive HTML.
Padel Heaven is a complete game in twenty kilobytes because it keeps making the same trade: replace a general mechanism with the specific answer the game needs. A projection of a few lines replaces a 3D engine, a fixed painting order replaces a depth buffer, a solved launch velocity replaces aiming, and one nearest-player rule replaces three agents. Each trade is visible and cheap, and each carries a precise cost that a headless replay can measure: the frame rate leaks into the physics, and the AI's shortcut for deciding whose ball it is fails at exactly the moment that makes padel padel — when the ball comes back off the glass. The fix for that is one expression. The general lesson is older than the browser: a simulation small enough to read in an afternoon is also small enough to run ten thousand times before anyone plays it.