Pokémon Pinball
Jupiter's 1999 Game Boy Color pinball game on two Pokémon tables, read through the pret/pokepinball disassembly.
On this page
Jupiter’s Pokémon Pinball has two tables, the Red and Blue fields. The atlas reads it through pret’s pokepinball disassembly, which builds the US and Australian ROM1. Labels and comments quoted here are pret’s.
The ball moves in 8.8 fixed point, gaining 11/256 of a pixel per frame of downward speed every frame2. Each axis is capped at about 7 pixels a frame, so a diagonal ball goes faster3. A wall’s angle comes from which of 16 points around the ball touch it, read from a table4. The ball bounces back at a quarter of its speed into the wall, and a hard hit shakes the rumble5.
A flipper swings in 5 frames6. Its push grows with how far from the pivot the ball touches it, and comes only while it’s swinging up7. The tilt buttons nudge the ball and never tilt out8. A ball saver runs for 20 seconds at the start of each ball, pausing while the ball’s x is 154 or more9.
In Catch ‘Em mode, a table mode, on the Red field, Voltorb hits flip the billboard’s 24 tiles to reveal a Pokémon10. The fourth registered hit on it catches it11. Every species gets 2 minutes12.
Mechanics
| Mechanic | Kind | How |
|---|---|---|
| Pinball physics | Masks and a normal table | 16 points around the ball tested against 1-bit masks; the touching run picks a normal from a 256-entry table, and the bounce keeps a quarter of the speed into the wall |
| Gravity | One gravity | 11/256 px per frame² on the ball, 8.8 fixed point |
| Flipper | By the contact point | 5 frames a swing; the push is the contact's distance from the pivot times the swing this frame, up to 12.6 px per frame at the tip |
| Tilt | Nudge only | three buttons nudge the ball a pixel a frame for 3 or 4 frames, with no tilt-out |
| Table mode | Timed | Catch 'Em mode: 2 minutes to reveal a Pokémon with 24 billboard tiles and hit it 4 times |
| Ball saver | Timed | 20 seconds at each new ball, two saves; 60 seconds without limit during Catch 'Em mode |
Every claim about this game, by concept (12)
References
- ^In the US and Australian Pokémon Pinball, the main loop runs the game's screen logic once per frame. During play that logic adds gravity, caps the velocity, handles the tilts and, on the lower half of a table, the flippers. It then tests for collisions, bounces the ball if one touches, and moves the ball last.Codepret/pokepinball
How we know
2114.master_loop 2115 call TickRumbleDuration 2116 call DoScreenLogic ⋮ 2119 rst AdvanceFrame 2120 jr .master_loop
home.asm lines 2114–2120 at 0a6f6dc · upstream ↗ 69GameScreenFunction_HandleBallPhysics: ; 0xd909 70; main loop for stage logic ⋮ 74 call ApplyGravityToBall 75 call LimitBallVelocity ⋮ 78 call HandleTilts ⋮ 81 callba nz, HandleFlippers ; only perform flipper logic on the lower half of stages ⋮ 86 call CheckStageCollision ⋮ 91 call CheckGameObjectCollisions ⋮ 111 call ApplyCollisionForces ⋮ 150 call MoveBallPosition
engine/pinball_game.asm lines 69–150 at 0a6f6dc · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^Gravity adds 11/256 of a pixel per frame to the ball's downward speed every frame while gravity is on. The ball's position and velocity are both 8.8 fixed point: whole pixels in the high byte, 256ths in the low.Codepret/pokepinball
How we know
2377ApplyGravityToBall: ; 0x2168 2378; Adds a constant to the pinball's y velocity. 2379 ld a, [wEnableBallGravityAndTilt] 2380 and a 2381 ret z 2382 ld de, $000b ; gravity added to y velocity every frame ⋮ 2392 ret
home.asm lines 2377–2392 at 0a6f6dc · upstream ↗ 271wBallXPos:: ; 0xd4b3 272; 8.8 fixed-point values in little-endian format ⋮ 278; 8.8 fixed-point values in little-endian format ⋮ 283wBallXVelocity:: ; 0xd4bb 284; 8.8 fixed-point values in little-endian format ⋮ 287; 8.8 fixed-point values in little-endian format
wram.asm lines 271–288 at 0a6f6dc · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^Each axis of the ball's velocity is capped on its own: a high byte above 7 becomes 7 and one below −7 becomes −7, the fraction kept, so the caps are 7 255/256 px per frame down or right and −7 up or left (computed), and a diagonal ball goes faster than a straight one. The move is limited again: a velocity of 5 px per frame or more moves the ball 4 255/256 px that frame, and one below −4 moves it 4 255/256 px the other way.Codepret/pokepinball
How we know
2394LimitBallVelocity: ; 0x2180 2395; Ensures that the ball's x and y velocity are kept under a threshold. 2396; The ball can travel at a greater max speed when moving diagonally, since it 2397; limits the x and y components independently. ⋮ 2406 cp 8 2407 ret c 2408 ld a, 7 ; max positive velocity ⋮ 2410 ret ⋮ 2413 cp -7 2414 ret nc 2415 ld a, -7 ; max negative velocity
home.asm lines 2394–2415 at 0a6f6dc · upstream ↗ 2436AddVelocityToPosition: ; 0x21c3 2437 ld a, [de] 2438 bit 7, a 2439 jr nz, .negativeVelocity 2440 cp 5 2441 jr c, .readLoByte 2442 ld bc, $04ff 2443 jr .updateBallPos ⋮ 2445.negativeVelocity 2446 cp -4 2447 jr nc, .readLoByte 2448 ld bc, -$04ff ⋮
home.asm lines 2436–2458 at 0a6f6dc · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^To find a wall, the game tests 16 points around the ball against 1-bit collision masks for the tiles under it. The first and last points of the longest run of touching points index a 256-entry table,
CollisionForceAngles, for the surface's normal angle, in 256ths of a turn. Two more tables with the same index push the ball back out of the wall.Codepret/pokepinballHow we know
2735 ld b, 16 ; length of wCollisionPointTestshome.asm line 2735 at 0a6f6dc · upstream ↗ 2878.determinedCollisionStatus 2879 ; e = maximum number of consecutive successful collision points 2880 ; d = the start and last index of that consecutive string of collision points (wCollisionPointTests) 2881 ; the hi nybble is the start index, lo nybble is the end index ⋮ 2892 ; Based on the start and end collision point indices, look 2893 ; up the corresponding normal angle for them. ⋮ 2896 ld hl, CollisionForceAngles 2897 add hl, de 2898 ld a, [hl] 2899 ld [wCollisionNormalAngle], a 2900 ; Again, based on the start and end collision point indices, apply 2901 ; position offsets to position the ball safely outside of the wall ⋮
home.asm lines 2878–2922 at 0a6f6dc · upstream ↗ 2345Cosine: ; 0x2147 ⋮ 2349 add $40 ⋮ 2357 and $7f ; ensure angle is between 0 and 180 degrees
home.asm lines 2345–2357 at 0a6f6dc · upstream ↗ 1CollisionForceAngles: ; 0x8817 2; This data has something to do with collisions. 3 db $C0
data/collision/collision_deltas.asm lines 1–3 at 0a6f6dc · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^On a hit, the game rotates the ball's velocity so the surface's normal points up. A ball moving away is left alone. A ball moving in bounces back at a quarter of its speed into the surface, plus an eighth for each step of the hit's amplification. Its speed along the surface gains half the stored spin, in 256ths of a pixel per frame; the new spin is that speed × 4 / 256. The velocity is rotated back. A speed of 3 px per frame or more into the surface also sets the rumble's duration to 1 and, unless a flipper was hit, plays a sound.Codepret/pokepinball
How we know
101 ld a, [wIsBallColliding] ; check for collision flag ⋮ 106 ; First, rotate the velocity vector into a standardized coordinate 107 ; system where the normal angle points upwards. 108 ld a, [wCollisionNormalAngle] 109 call RotateVector 110 ; Apply the collision forces to the ball in the rotated coordinate system. 111 call ApplyCollisionForces ⋮ 144 ; Rotate the velocity vector back to the regular coordinate system, and 145 ; set the ball's velocity to the resulting vector. 146 ld a, [wCollisionNormalAngle] 147 call NegateAngleAndRotateVector 148 call SetBallVelocity
engine/pinball_game.asm lines 101–148 at 0a6f6dc · upstream ↗ 2528ApplyCollisionForces: ; 0x222b ⋮ 2542 ; Early exit if the ball is traveling away from the collision normal. 2543 bit 7, d 2544 jr nz, .exit ⋮ 2547 cp 3 2548 jr c, .applyforces 2549 ; The ball collided hard because its y velocity is a 2550 ; large value. Shake the rumble pack, and play a little 2551 ; sound effect to enhance the collision. 2552 ld a, $ff 2553 ld [wRumblePattern], a 2554 ld a, 1 ⋮
home.asm lines 2528–2625 at 0a6f6dc · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^Each flipper's position runs from 0 to 15 in 8.8 fixed point. Holding its button adds 0x0333 (3.2 positions) each frame and releasing it subtracts 0x0333, so a flipper swings fully up in 5 frames and fully down in 5 (computed). While the flippers are disabled, a held button counts as released. A flipper resting at either end records no change.Codepret/pokepinballrevised
How we know
14UpdateFlipperStates: ; 0xe118 ⋮ 20 ld hl, wKeyConfigLeftFlipper 21 call IsKeyPressed2 22 ld hl, -$0333 23 jr z, .checkLeftStateBounds 24 ld a, [wFlippersDisabled] 25 and a 26 jr nz, .checkLeftStateBounds 27 ld hl, $0333 28.checkLeftStateBounds ⋮ 30 and a ⋮ 35 cp $f ⋮
engine/pinball_game/flippers.asm lines 14–76 at 0a6f6dc · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^Flipper collisions come from precomputed tables. For each flipper position from last frame's to this frame's, a 48 × 32-pixel grid gives the contact's distance from the pivot and the surface normal there; the right flipper reads the left's grid mirrored. A hit bounces the ball with amplification 1, then pushes it out along the normal by
FlipperRadiusMagnitudesat that distance times 4 × the flipper's change this frame. A still flipper adds no push, and one moving down adds none either. The push at the tip of a rising flipper is 12.6 px per frame (computed); next frame's cap holds each axis to under 8.Codepret/pokepinballHow we know
202ReadFlipperCollisionAttributes: ; 0xe25a 203 ldh a, [hBallXPos + 1] 204 sub 43 ; check if ball is in horizontal range of flippers 205 ret c 206 cp 48 207 ret nc ⋮ 211 sub 123 ; check if ball is in vertical range of flippers 212 ret c 213 cp 32 214 ret nc ⋮ 218.collisionCheckLoop ⋮ 275.collision ⋮
engine/pinball_game/flippers.asm lines 202–297 at 0a6f6dc · upstream ↗ 167CheckRightFlipperCollision: ; 0xe226 168; ball is in right half of screen ⋮ 171 ld a, [hli] ; Invert the ball's x position, so that the flipper collision bytes are mirrored.
engine/pinball_game/flippers.asm lines 167–171 at 0a6f6dc · upstream ↗ 622HandleFlipperCollision: ; 0xe442 ⋮ 625 ld a, 1 ⋮ 637 ld hl, FlipperRadiusMagnitudes ⋮ 648 rl d ⋮ 650 rl d ; multiplied flipper state change by 4 651 call CalculateFlipperYForce ⋮ 660 cpl ; mirror the normal angle across the y axis ⋮ 664 ld a, 1 665 ld [wCollisionForceAmplification], a ⋮ 669 ; don't apply any y force if the ball is being forced downwards into the flipper
engine/pinball_game/flippers.asm lines 622–673 at 0a6f6dc · upstream ↗ 423CalculateFlipperYForce: ; 0xe379 424; Calculates the y force that's added to the ball's velocity when colliding with a flipper. 425; Flippers rotate around a point, and the further away from that rotation point that ball is, the 426; higher the magnitude value is (bc). This magnitude is multiplied by the flipper state change to
engine/pinball_game/flippers.asm lines 423–430 at 0a6f6dc · upstream ↗ 800; Each entry corresponds to a distance from the flipper's rotation point. 801FlipperRadiusMagnitudes: ; 0xe538 802 dw $0000 803 dw $000C ⋮ 818 dw $00F8 819 dw $00FC 820 dw $00FC 821 dw $00FC 822 dw $00FC 823 dw $00FC 824 dw $00FC 825 dw $00FC ⋮
engine/pinball_game/flippers.asm lines 800–833 at 0a6f6dc · upstream ↗ 115 ; de -= *wFlipperYForce 116 ld hl, wFlipperYForce 117 ld a, [hli] 118 ld h, [hl] 119 ld l, a 120 ld a, e 121 sub l 122 ld e, a 123 ld a, d 124 sbc h 125 ld d, a
engine/pinball_game.asm lines 115–125 at 0a6f6dc · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-25.
Checked by a second AI model, 2026-10-22.
Checked by a second AI model, 2026-10-25.
- ^Pokémon Pinball's tilt is a nudge with no tilt-out: the tilt routines keep no warning count and never end a ball. Holding a tilt button (left, right, or up; D-pad Down, B, and Select by default) runs its counter and screen shake for 3 frames (4 for up), and on each of those frames moves the ball 1 pixel if the ball is visible and gravity and tilt are on. That tilt then waits until the button is released and its counter has run back to 0. While a tilt is pushing, a ball touching a surface gets a velocity from a table for that tilt, read by the normal angle. Left and right together cancel.Codepret/pokepinballrevised
How we know
1HandleTilts: ; 0x3582 ⋮ 5 ret ⋮ 7HandleLeftTilt: ; 0x358c 8 ld a, [wLeftTiltReset] 9 and a 10 jr nz, .tiltCoolDown 11 ld hl, wKeyConfigLeftTilt 12 call IsKeyPressed2 ⋮ 14 ld a, [wLeftTiltCounter] 15 cp 3 16 jr z, .startCoolDown ⋮ 24 ld a, [wPinballIsVisible] ⋮
home/tilt.asm lines 1–240 at 0a6f6dc · upstream ↗ 101 ld a, [wIsBallColliding] ; check for collision flag 102 and a 103 jr z, .moveBallPosition 104 call ApplyTiltForces
engine/pinball_game.asm lines 101–104 at 0a6f6dc · upstream ↗ 1080DefaultKeyConfigs: ; 0xca55 ⋮ 1084 db D_DOWN, $00 ; wKeyConfigLeftTilt 1085 db B_BUTTON, $00 ; wKeyConfigRightTilt 1086 db SELECT, $00 ; wKeyConfigUpperTilt
engine/options_screen.asm lines 1080–1087 at 0a6f6dc · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^Each table starts with a 20-second ball saver, and every ball lost for good starts a new one. While it runs, a drained ball is saved: the first save leaves the timer alone, and the second cuts it to 1 second and 1 frame. The timer counts down 60 frames a second, and not while the ball is at x 154 or more. Set to 20 seconds, it lasts 1,259 frames, about 21 seconds, if the ball stays left of x 154 throughout (computed). The slot cave can award 30, 60, or 90 seconds instead.Codepret/pokepinballrevised
How we know
1Start20SecondSaverTimer: ; 0xdbba ⋮ 6 ld a, 59 7 ld [wBallSaverTimerFrames], a 8 ld a, 20 9 ld [wBallSaverTimerSeconds], a 10 ld a, $2 11 ld [wNumTimesBallSavedTextWillDisplay], a
engine/pinball_game/ball_saver/ball_saver_20.asm lines 1–12 at 0a6f6dc · upstream ↗ 1HandleBallLossRedField: ; 0xdd76 2 ld a, [wBallSaverTimerFrames] 3 ld hl, wBallSaverTimerSeconds 4 or [hl] 5 jr z, .youLose 6 ld a, [wNumTimesBallSavedTextWillDisplay] 7 bit 7, a 8 jr nz, .skip_save_text 9 dec a 10 ld [wNumTimesBallSavedTextWillDisplay], a ⋮ 15 jr nz, .skip_save_text 16 ld a, $1 ⋮
engine/pinball_game/ball_loss/ball_loss_red_field.asm lines 1–31 at 0a6f6dc · upstream ↗ 54UpdateBallSaverState: ; 0x146a9 55 ld a, [wBallSaverTimerFrames] 56 ld hl, wBallSaverTimerSeconds 57 or [hl] ;skip if timer ran out 58 ret z 59 ld a, [wBallXPos + 1] 60 cp 154 ;if high? Byte of ball X pos is >= 154, don't update timers 61 jr nc, .SkipSecondsOrFramesUpdate 62 ld a, [wBallSaverTimerFrames] 63 dec a 64 ld [wBallSaverTimerFrames], a 65 bit 7, a ⋮
engine/pinball_game/object_collision/red_stage_resolve_collision.asm lines 53–75 at 0a6f6dc · upstream ↗ 43 callba Start20SecondSaverTimerengine/pinball_game/stage_init/init_red_field.asm line 43 at 0a6f6dc · upstream ↗ 161SlotRewards_CallTable: ; 0xeeca 162 dw Start30SecondSaverTimer 163 dw Start60SecondSaverTimer 164 dw Start90SecondSaverTimer
engine/pinball_game/slot.asm lines 161–164 at 0a6f6dc · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^In Catch 'Em mode on the Red field, each Voltorb hit flips 4 of the billboard's 24 tiles and scores 100,000, so 6 hits reveal the Pokémon from a blank board (computed). The Pokémon appears once all 24 are flipped and the ball is on the table's lower half.Codepret/pokepinballrevised
How we know
231HandleVoltorbCollision_CatchemMode: ; 0x20230 resolve hitting a voltorb in catch mode? 232 ld a, [wNumberOfCatchModeTilesFlipped] 233 cp $18 ⋮ 251 ld a, [wNumberOfCatchModeTilesFlipped] 252 add $4 ;then add 4 to FlippedCount, clamp to 24 253 cp $18 ⋮ 258 cp $18 ⋮ 264 ld bc, OneHundredThousandPoints
engine/pinball_game/catchem_mode/catchem_mode_red_field.asm lines 231–264 at 0a6f6dc · upstream ↗ 28Func_20041: ; 0x20041 29 ld a, [wNumberOfCatchModeTilesFlipped] 30 cp $18 31 jr nz, .NotDone 32 ld a, [wCurrentStage] 33 bit 0, a 34 jr z, .NotDone
engine/pinball_game/catchem_mode/catchem_mode_red_field.asm lines 28–34 at 0a6f6dc · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^Once the Pokémon appears, each registered hit scores 300,000 and steps a counter from 0 to 3, lighting part of the CATCH bar; a hit with the counter at 3 catches it, so the fourth registered hit catches. Mew counts a hit only when a separate byte of its own rolls over at 256, so it would need 1,024 hits from a zero byte (computed). When the timer runs out on Mew, the game marks Mew caught in the Pokédex anyway.Codepret/pokepinballrevised
How we know
97 ld a, [wBallHitWildMon] 98 and a 99 jp z, .BallDidntHitMon ;if no ball hit(?), jump ⋮ 106 ld a, [wCurrentCatchEmMon] 107 cp MEW - 1 108 jr nz, .notMew 109 ld a, [wNumMewHits] 110 inc a 111 ld [wNumMewHits], a 112 jr nz, .Not256MewHits 113.notMew 114 ld a, [wNumMonHits] ⋮
engine/pinball_game/catchem_mode/catchem_mode_red_field.asm lines 96–148 at 0a6f6dc · upstream ↗ 99 ld a, [wBallHitWildMon] ⋮ 116 ld a, [wNumMonHits] 117 cp $3
engine/pinball_game/catchem_mode/catchem_mode_blue_field.asm lines 99–120 at 0a6f6dc · upstream ↗ 866Func_10611: ; 0x10611 ⋮ 884Data_1062a: 885 dw Data_10630 886 dw Data_10638 887 dw Data_10640
engine/pinball_game/catchem_mode.asm lines 866–888 at 0a6f6dc · upstream ↗ 212CheckIfCatchemModeTimerExpired_RedField: ; 0x201f2 ⋮ 221 ; Automatically set Mew as caught, since you can't possibly catch it 222 ld a, [wCurrentCatchEmMon] 223 cp MEW - 1 224 jr nz, .asm_2021b 225 callba SetPokemonOwnedFlag 226.asm_2021b
engine/pinball_game/catchem_mode/catchem_mode_red_field.asm lines 212–227 at 0a6f6dc · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^Every Pokémon gets the same 2 minutes to be caught: all 151 entries of
CatchEmTimerDataare 0 seconds and 2 minutes. Starting the mode also starts a 60-second ball saver that saves without limit, and the saver from before is restored when the mode ends.Codepret/pokepinballrevisedHow we know
1CatchEmTimerData: ; 0x12a22 2; Each 2-byte entry represents the total time you have to catch a Pokemon. 3; They're all exactly 2 minutes, 0 seconds, but they can be changed to make 4; different pokemon have different times, in order to add difficulty. 5; First byte = seconds value 6; Second byte = minutes value 7 db $00, $02 ; BULBASAUR ⋮ 157 db $00, $02 ; MEW
data/catchem_timer_values.asm lines 1–157 at 0a6f6dc · upstream ↗ 123 ld hl, CatchEmTimerData ;contains how long each mon stays on screen, all are 2 minutes by default ⋮ 129 callba StartTimer 130 callba InitBallSaverForCatchEmMode
engine/pinball_game/catchem_mode.asm lines 118–131 at 0a6f6dc · upstream ↗ 1InitBallSaverForCatchEmMode: ; 0xdbd4 ⋮ 10 ld a, $ff ⋮ 14 ld a, 60 15 ld [wBallSaverTimerSeconds], a 16 ld a, $ff 17 ld [wNumTimesBallSavedTextWillDisplay], a 18 ret ⋮ 20RestoreBallSaverAfterCatchEmMode: ; 0xdc00 ⋮ 24 ld [wBallSaverTimerSeconds], a ⋮ 26 ld [wNumTimesBallSavedTextWillDisplay], a
engine/pinball_game/ball_saver/ball_saver_catchem_mode.asm lines 1–32 at 0a6f6dc · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
Sources
Code
- pret contributors. 2026. “pokepinball: disassembly of Pokémon Pinball.” pret. Commit 0a6f6dc. pret/pokepinball @ 0a6f6dc · licence, all uses