Bug or feature?

Writing about randomness reminded me of an interesting bug in the first commercial game I ever released. Extreme G was a futuristic racer for the Nintendo 64. Each vehicle had a limited number of turbo boosts, which increased your speed as long as you drove cleanly, but cut out as soon as you clipped the edge of the track. It was impossible to corner cleanly at turbo speed, so the best tactic was to save your boosts for the longest straight section, while trying to knock boosting opponents off the track.

Ash (the lead programmer) wrote some AI code that decided when the computer players should use their turbo. They would boost when they reached a straight piece of track, and also (for variety) at occasional randomly chosen intervals.

Shortly after the game came out, we read a review that went something like:

"We especially loved the aggressive AI, which does everything in its power to stop you overtaking. If you pass a computer player, they will fight back by firing their turbo, even midway through a turn where that is sure to cause a massive pileup. Perhaps not the best winning strategy, but an awesome f-you attitude!"

"Hah!", quoth Ash, "what a foolish reviewer! That's not how it works. I bet the random number generator just happened to trigger its turbo at the same time he was overtaking, and he read too much into that. My actual AI code is nowhere near so subtle."

But then he went back to look at this actual AI code ๐Ÿ™‚

A universal challenge of racing game design is how to keep the pack of vehicles close together. If they become too widely separated, the player can be left racing on what appears to be an empty track, with the computer players out of sight in front or behind them, which is no fun at all. In Extreme G, this problem was exacerbated by two things:

  • For performance reasons, we could not afford as many computer vehicles as we ideally wanted, so the pack was overly small in the first place.
  • Weapons and turbo boosts made the gameplay less predictable than in a pure racer, tending to spread out the vehicles.

To help keep the pack together, Ash biased the turbo AI based on race position. When a computer player was in front of the human they would rarely use their turbo, but if they were far behind they would fire it in an attempt to catch up.

What he meant to write was:

    if (computerPlayer is behind human)
        ushort difference = human.TrackPos - computerPlayer.TrackPos;

        if (difference > catchUpThreshold)

The TrackPos field held a 16 bit counter of how far around the track each vehicle was, normalized so that 0 represented the starting line, 32768 was halfway around, and 65535 was a full lap nearly back to the start. This format was convenient because the rounding rules of integer arithmetic automatically handled the modulo operations necessary when comparing positions from different laps, or from either side of the start line.

Thanks to an order of processing bug, what actually happened was:

    if (computerPlayer is behind human)
        ushort difference = human.TrackPos - computerPlayer.TrackPosFromPreviousFrame;

        if (difference > catchUpThreshold)

This worked as intended, except when you overtook an AI vehicle:

  • Computer was in front of human
  • Now human is in front, so this code kicks in
  • Because it erroneously uses the computer position from the previous frame, the position difference comes out negative
  • Because the type is unsigned, this is interpreted as a large positive integer
  • It looks like the computer is a long way behind, so the turbo is triggered


But the reviewer was absolutely right. Although accidental, the resulting behavior was good gameplay. We left this bug unchanged in the sequel.

Comments (27)

  1. ppindia says:

    Here is the advantage of managed language like Java or C#. No need to worry about the Registers!!!

    1. Kyle Strand says:

      @ppindia Other comments have pointed out that this doesn’t actually have much to do with registers, but what I’m wondering is why you think it’s possible or desirable to use a managed language on such a specialized, resource-limited platform like the N64.

    2. They didn’t want shit performance

  2. Giniedp says:

    hehe this is a nice little story

  3. Lintfordpickle says:

    lol – I enjoyed reading this too.  

    Wish some of my bugs had such a happy ending ๐Ÿ™‚

  4. Cardin says:

    haha, that was really a unique AI behaviour. Accidents are the stuff of ingenuity after all!

  5. Benoît says:

    I’m not sure to understand how this is possible. If you use the computer position from the previous frame you shouldn’t have a negative difference, let me explain myself by illustrating your example :

       *  Computer was in front of human, let’s say human.TrackPos = 32000, computer.TrackPos = 32001

       * Now human is in front, so this code kicks in => human.TrackPos = 32003, computer.TrackPos = 32002, computer.TrackPosFromLastFrame = 32001

       * Because it erroneously uses the computer position from the previous frame, the position difference comes out negative

    => here is my problem : the difference is just slightly more than what it should be (2 instead of 1) but isn’t negative

    Am I wrong or was the bug using layer.TrackPosFromLastFrame instead of player.TrackPos ?

    But funny story by the way ๐Ÿ™‚

  6. Dean Harding says:

    I know it’s been a while (thanks Raymond!), but to Benoît, perhaps it was the other way around:

       ushort difference = human.TrackPosFromPreviousFrame – computerPlayer.TrackPos;

    That would result in a negative number…

  7. Tim! says:

    @ppindia: this doesn’t have anything to do with registers.  Regardless of managed vs. unmanaged languages, in a strongly-typed language you still have to deal with finite-valued types.  ushort’s maximum value is 65535 in both C and C#, and after "ushort foo = 65535 + 1" foo contains 0.

    The bug here appears to be that the AI was determining its behavior before it had applied its velocity and updated its position.

  8. John says:

    My guess is that in Nintendo 64 land the refreshes didn’t happen frequently enough to use the scale Benoit is indicating.

    Taking the example to a drastic extreme in the other direction, at one quarter of the way around the track the computer (16385) is ahead of the human (16384).

    At one half way around the track, the human (32768) passes the computer (32767).  You would expect the computer to see a difference of 32768-32767=1.

    Using the ‘previous frame’ calculation, you get 32768-16385=16383 which is one quarter of the way back around the track.  Hence, the computer fires the turbo to catch up.

    I don’t see where the negative value comes in, either.  However, the turbo behavior makes perfect sense.

  9. vczilla says:

    To Benoit:

    I think it can be done if the computer actually goes backward (remember it is not a pure racing game)

    1 – computer is in front: human.trackPos=32000,computer.trackPos=32005

    2 – human catches up and computer crashes and ends up backward:


    we have human.trackPos-computer.lastTrackPos=32004-32005=-1!

  10. Krunch says:

    > @ppindia: this doesn’t have anything to do with registers.

    > Regardless of managed vs. unmanaged languages, in a

    > strongly-typed language you still have to deal with

    > finite-valued types.  ushort’s maximum value is 65535

    > in both C and C#, and after "ushort foo = 65535 + 1"

    > foo contains 0.

    While ppindia obviously didn’t get it, this has nothing to do with strong typing. There are strongly-typed languages that do have infinite types (see the Integer type in Haskell for example) and I am not sure there are actually weakly-typed language that have infinite numerics builtin.

  11. Myria says:

    When you intentionally don’t fix a bug like this, *please* put a comment in the code saying that it is a bug but provided desirable behavior.  The people who later maintain your code will thank you.

  12. MauiCat says:

    I wish I had these tools and toys in 1967.

    Register architecture always was an important limit and tool.

    I once wrote an IBM/360 machine language one line loop to compute the next largest power of two for a binary search.

    In those days we had 1.2M (Yes Megabytes) of memory, no disk drives (not fast, not reliable), and the machines were so big… but I digress.

    Our compiler could specify "word" sizes in any number of bits.  If your number ranged from 0-7 then 3 bits was enough.  If you had a 5 bit number, the two could be packed in the same byte.  The JOVIAL (Jules Own Version of the International Algorithmic Language) compiler did this auto-magically.

    Since the entire operating system and database was memory resident, and the CPU was slow (by today’s standards) saving microseconds counted.

    We ran elaborate tests in a real production environment but the bug was not diagnosed.

    A few days before we released this system to the world I found MY BUG in a code review.  It was an uninitialized register that provided the very same sort of random seed as in the racer game.  

    Fortunately we fixed it timely, but it scared the poop out of me and changed my life forever.

    The application wasn’t a game, and must not fail. It ran on IBM/9020s, modified IBM/360 computer which were the prototype for the 370 series.  

    It was (still is) the FAA Air Traffic Control system.  We tested in the real Jacksonville, FL air traffic control center since it was not 24/7.

    My bug could have killed people, not just computer race cars.  Oh, boy would I have loved the tools we have today.

    This is my first post to this blog.  If I’m out of line please feel free to flame me.  I learn fast.  

  13. Nobody says:

    So, the funny story is actually that you never even tested the game before releasing it? ๐Ÿ™‚

  14. Mat says:

    Did you have anything to do with this PS2 title? I used to work in a games shop and, for whatever reason, that game was always really cheap. I bought it on a whim one day and loved it – it was *so* fast. Anyway, I actively stopped people buying Wipeout Fusion until they had tried XG3. It saved people about £15-£20 and not one of them came back to exchange it.

  15. Tim says:

    I remember playing this game I loved it. It was the first game that made me feel like I was actually going fast and the AI was actually working against me…..only now do I realize that was a lie. Good type of bug to have though I guess

  16. Shirondale says:

    I still have the original Extreme G and have not placed much attention to this behavior until now. That aggressive behavior actually happens! If you have to have a bug, it may as well be like this one.

    Did you work on all of the Extreme G games? I started with XGRA and worked my way backwards to see how it progressed. I love this series.

  17. Bryan Ma says:

    Great story, and brought back good memories of Extreme G!  Narrative fallacy has filled in the gaps in many older games for me, I’m sure – but it’s amusing to see that the assumption of the reviewer was actually correct.

  18. Bobby says:

    Please reconsider your web site’s layout.  My screen is at 1024 by 768 and your code still got cut off (and there wasn’t even a horizontal scroll bar).  

  19. ath1 says:

    For those who don’t understand how the negative value comes in, I think it has to do with the size of the types being used. A unsigned short = 16 bits = It can hold values up to 65,536. Once you exceed that number, it overflows, and the value turns negative. ๐Ÿ™‚

  20. MauiCat says:

    Actually you are close, but no cigar.

    1. 16 bit unsigned can take on values from 0 to 65,535, e.g. hex 0000 – FFFF.  Add one to 0xFFFF and you wind up with 0x0000, not 65536.

    2. An unsigned cannot go negative by definition.

    3. A signed short of 16 bits can range from -32768 to +32767, 0x1000 to 0x7FFF since the high order bit is the sign bit.

    My arithmetic is rusty — I’m 67 so forgive a bit and correct me if I have erred.

  21. R.A. says:

    now I dont know how to code, at all, but I think I can see how the negative may come in? from my simple perspective that is.

    -Check Human position = 1499,

    check computer position 1500 = ahead

    -Check human position = 1510

    Check computer position = 1509

    report human = ahead

    -human ahead reported

    computer checks last frame human position

    vs computer position for distance ahead

    previous frame

    human = 1507

    computer = 1508


    human = ahead

    human position – computer position (1507 – 1508) = -1 or ahead one lap coming up behind computer

    boost to catch up

    like I said I have no actual knowlage on the subject, but thats just my take on it by the info given, or my wild guess if you would

  22. eoin says:


    The post actually says it only uses the computer’s previous position, not the human’s.

    ushort difference = human.TrackPos – computerPlayer.TrackPosFromPreviousFrame;

    Since at the point where this code is executed, human.TrackPos is greater than computerPlayer.TrackPos and computerPlayer.TrackPosFromPreviousFrame I don’t see how it could be "negative".

    Using your example and the code above we’d have something like:

    1510 – 1508 = 2.

    The only way I can see the negative "feature" happening, as has been mentioned already above, is if it was indeed the human’s previous frame position being used, as opposed to the computer’s.

  23. Paul Richardson says:

    I worked on XG3 and XGRA and I can say that the AI wasn’t that much different on those titles ๐Ÿ™‚

  24. Cam Brown says:

    All Microsoft features are bugs.

  25. Jesรบs Gรณmez says:


Skip to main content