Candystand.com · 2001 · Shockwave

Wiffle Ball, Decompiled

Candystand's Wiffle Ball was a fixture of early-2000s browser games. We decompiled its code from the copy preserved in Flashpoint. Two decades later, it turns out the game had been quietly breaking its own rules the whole time.

15frames per second
2outs per side
53°of fair territory
0ways to end in a tie
~4.5klines of game code
12·13·01build folder date

The box score

FindingWhat it means in playKind
Amanda and Todd are swappedEach plays with the other's ratingsBug
Hit-by-pitch is just a ballWith 3 balls, getting hit does nothingBug
The silent walk-in runNo points, and it can't win the gameBug
"You Tied" never appearsThe art exists; the game can't reach itQuirk
The bat ignores heightShoelace-high or head-high, you can hit itSecret
You get 3 frames, the CPU gets 1Your timing window is triple the computer'sSecret
Your mouse steers after the swingKeep moving through contact to aimSecret
Launch angles are randomA whole table of hit types is thrown awayBug
The flutter is a zig-zagA sine bug flattens the wiffle wobbleBug
How the CPU decides to swingNever at slow pitches; always down the middleSecret
The school's missing curbOne side of the street has no collisionBug
The backwards MercedesIts hitbox drives the other wayBug
Nothing is roundHydrant, wagon and bike collide as boxesBug
The invisible balloonA typo picks a balloon that doesn't existBug
The CPU fielder chases a ghostAfter a grounder bounces, it runs to the old spotBug
An aim-assist that never shippedIt learns your misses, then ignores themLeftover
Secret keysN restarts; Ctrl+V shows the build; ~30 dev keysSecret

The big one

Amanda and Todd got each other's stats

Batters · initBatters / addBatter, and Init Constants · prepareMovie

The select screen says Amanda "may not be a pro at bat, but watch out for those killer pitches!" and that Todd is "a good all-around player." In the game itself, it's the other way around.

Each player's ratings come from one line: addBatter(hand, fielding, hitting, pitching, …). "Pitching" is the random scatter added to every pitch, so lower is better.

WhoFieldingHittingPitch scatterCard says
Kent8084Strong hitter, decent glove ✓
Tom90108Slugger ✓
Amanda (as shipped)9056Weak bat, killer arm ✗
Todd (as shipped)9532All-rounder ✗

Todd, the "all-rounder," secretly had the weakest bat, the best glove and the most accurate arm in the game. The select-screen rating bars agree. They use a consistent scale (fielding 80 → 4 bars, 90 → 5, 95 → 6; hitting 1 bar per point). Amanda's card shows 6 fielding bars and 3 hitting bars, which is exactly Todd's in-game row.

The cause is in the setup code. The developer-only branch loads the players in the order Ken, Tom, Tod, Ada. The web build loads Ken, Tom, Ada, Tod. The stat rows match the first order, so they were apparently written for it and never swapped.

-- authoring mode (the developers' copy)
playerfilenames = ["cnwb_Ken", "cnwb_Tom", "cnwb_Tod", "cnwb_Ada"]
-- the shipped web build
playerfilenames = ["cnwb_ken.cct", "cnwb_tom.cct", "cnwb_ada.cct", "cnwb_tod.cct"]

In play: if you picked Amanda for her pitching, you got a mid-pack arm. If you wanted a slugger who could also field, Todd was never it. The batting hot zones and pitch mixes are swapped along with them.

Rules the code bends

Hit-by-pitch is just a ball, and with three balls it's nothing

Game Code · gameLogic, #pitchCalled

Getting plunked never sends you to first. It adds one ball to the count. If you already have three balls, the game skips even that, so a hit-by-pitch can never walk you.

#hitbatter:
  if ballcnt < 3 then
    ballcnt = ballcnt + 1
  end if

The silent walk-in run

Game Code · gameLogic, the walk branch

Walk with the bases loaded and a run scores, but through a shortcut. Hits award points for runs and check for a walk-off win. A walked-in run does neither. If the home side walks in the winning run in the last inning, play continues until the second out ends it.

if bases[3] = 1 then
  runs[batter] = inithackv(readhackv(runs[batter] + 1))   -- no addScore, no game-over check

"You Tied" can never appear

Game Code · CallOut

The game ships with three end-of-game banners: You Win, You Lose and You Tied. Whenever the score is level at the end of the last bottom half, the game quietly adds an inning (inningend + 2). It does that forever, so the tie banner is unreachable art.

if (innings >= inningend) and (runs[1] = runs[2]) and ((innings mod 2) = 1) then
  inningend = inningend + 2
end if

Batting secrets

The bat ignores height

Batters · showBatter

Contact checks three things: the ball is crossing the plate (from 2.5 ft in front of it to half a foot behind), it hasn't bounced, and it's within 2 feet left or right of your bat's target. Its height is never tested. A pitch at your shoelaces or over your head is just as hittable as one at the belt.

if (ball[3] > -0.5) and (ball[3] < 2.5) then        -- depth
  if not bounceflg then
    if deltaZ < 0 then
      if (ball[1] > (-2 + targetx)) and (ball[1] < (2 + targetx)) then   -- side to side
        -- ...contact. ball[2] (height) is never checked.

You get three frames to make contact. The CPU gets one.

Batters · showBatter

The swing animation runs frame by frame. A human batter can connect on swing frames 7, 8 or 9; the computer only on frame 8. At 15 frames per second, that's a 200 ms window for you and 67 ms for the CPU.

if ((animbatter >= 7) and (animbatter <= 9) and human batting) or (animbatter = 8) then

Your mouse keeps steering after you let go

Batters · batball, More Game Code · strength

Swing power and direction come from how fast the mouse moved over the last eight frames. The game measures that at the moment of contact, not at the moment you released. Moving the mouse sideways through the swing pulls the hit left or right.

Every launch angle is random

Batters · batball

The code has a table meant to make high pitches fly and low pitches skid (grv), and it applies it. Five lines later, every hit's vertical speed is overwritten with a random number from 1 to 240.

ball[10] = getAt(grv, ind) * 20.0 * speedfactor   -- per-zone launch
...
ball[10] = random(240)                             -- ...replaced for every hit

The CPU's hit power has a similar problem. Its table [14, 16, 14, 18, 22, 18, 10, 12, 10] goes through * 2 / 24, and Lingo does that in whole numbers. Every entry collapses to 1 except the two 10s, which collapse to 0. So every CPU hit leaves the bat at the same speed, and on low-inside or low-outside pitches it has no forward speed at all.

The wiffle-ball flutter is a zig-zag

Init Constants · qsin, More Game Code · moveBall

After contact, the ball is supposed to wobble in a little corkscrew, the signature wiffle-ball movement. The game builds it from a 72-entry cosine table, reading sine by shifting the index. It shifts by 180 entries, which wraps around to 180 degrees, so "sine" returns minus-cosine and the corkscrew flattens into a diagonal line. The direction is meant to be random, but it's set in a local variable, so the wobble always turns the same way.

on qsin rot2
  return getAt(cosTable, (((rot2 / 5) + 180) mod 72) + 1)   -- a 90° shift would be 18
end

How the CPU batter decides to swing

Batters · battingAlgorithm

The computer never swings at a slow pitch. Don't get excited, though: a pitch that slow hits the dirt, and a bounced pitch is always a ball. We threw 96 soft tosses aimed everywhere from the middle to the top of the zone. The CPU took all 96, and all 96 were balls.

The opposite holds down the middle. The CPU forces a swing at any pitch headed for the center square. In our test it swung at all 96 hard pitches down the middle and put 88 of them in play. Pitch to the corners.

if predictedZone = 5 then rnd = 1000                  -- dead center: never take
if rnd < 25 then flg = 0
if deltaZ > (-0.6 * speedfactor) then flg = 0          -- too slow: never swing

Field glitches

Morris School is missing a curb

collisons (sic) · cobjects

The left curb is defined, then immediately overwritten by the right curb under the same name. Balls that should clatter off the left curb roll right through it.

obj05 = [-22, -1, 0, -17, 0.5, 45, #rectangle, 0, #concrete, #leftCurb]
obj05 = [17, -1, 0, 22, 0.5, 45, #rectangle, 0, #concrete, #rightCurb]

The black Mercedes on Larkin Street drives backwards (invisibly)

background · animbackground, collisons · DetectCollisions

Most vehicles drive left to right. The black Mercedes drives the other way, so its sprite is mirrored: it's drawn at 500 − x. Its collision box isn't mirrored, so the thing you can actually hit drives the opposite direction of the car you see.

Separately, the collision check reuses one position offset for every vehicle part: whichever vehicle was measured last. So even the delivery trucks' hitboxes sit slightly out of step with their pictures.

Nothing is round

collisons · DetectCollisions

The fire hydrant, the red wagon and the bicycle are tagged #cylinder, but the code that handles round objects is labeled #cylinderd. They never match, so all three bounce balls like boxes.

The invisible balloon

background · SetBackground

The yard and school have hot-air balloons drifting overhead, in pairs that shouldn't match colors. When they would match, the code meant to pick balloon 5 but typed 53. There is no balloon 53, so that balloon is invisible until it drifts off-screen and comes back as a new one. Elsewhere, the same fix correctly uses 5.

if bobjectn[1 + (x * 2)] = bobjectn[2 + (x * 2)] then
  bobjectn[2 + (x * 2)] = 53
end if

The CPU fielder chases a ghost

move pitcher · movepitcher

When the computer fields a grounder that has bounced, it feeds the ball's position into the wrong conversion: screen-to-world instead of world-to-screen. It then reads two values that conversion never fills in. So after a grounder bounces, the CPU fielder keeps running to where it first predicted the ball would go.

Leftovers and secrets

An aim-assist that never shipped

Batters · callStrikeZone, Game Code · mouseDownCode

Every pitch, the game measures how far your throw missed the spot you clicked and saves a correction for that batter, moving 70% of the way toward your error. The only code that reads those corrections loads them into two variables and never uses them. It's an unfinished feature: the game studies every pitch you throw and does nothing with what it learns.

Secret keys

Game Code · keyDown, Common Code · showversion

N jumps back to the character select at any time. That one works in the shipped game.

Hold Ctrl and tap V to show the build number. The preserved copy is V0802.2.

There's a pause key that can never work. It's written as the two-character string "pp", which a single keypress never equals. It also requires a #play state and an "ending" frame label, and neither exists.

About thirty developer keys are still in the code, switched off by debug = 0. They switch camera views, swap fields and players, and run pitch and cannon test rigs.

More from the files

Code names. The select screen's timeline is organized by labels named Stuart, Homer, Robin and Justin. Those are Kent, Tom, Amanda and Todd.

Borrowed code. The loading screen still tries to show "spacenation boy small" and two instruction images left over from another game's loader. They don't exist here, so nothing appears.

Anti-cheat. Scores sat in memory as the real value plus a random 5,001–10,000, to defeat memory editors. The select screen also refuses to run (ERROR 21 / ERROR 22) unless the web page hosting it hands over a copyright string. That was Candystand's protection against other sites hot-linking its games.

The sound board. Every sound effect lives in a small Flash movie hidden inside the Shockwave file. The game plays a sound by jumping that movie to a frame, and ten volume levels exist as separate frames. One car-alarm sound points one frame early and plays a beat late.

House rules (these are on purpose)

Outs per half-inning2 (the help screen says so)
Fair territory26.6° either side of center (|x / z| > 0.5 is foul), a 53° wedge instead of baseball's 90°
Hit typeDecided only by distance: under 23 ft single, under 47 double, under 72 triple, 72+ home run
Bounced pitchAlways a ball
FieldersOne: the pitcher fields everything
Speed15 frames per second, with all physics per frame. On a slow 2001 PC, the game ran in slow motion.
← All case files Department of Cool Shit · RSS