Bug or feature?

Bug or feature?

  • Comments 23

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)
            FireTurboBoost();
    }

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)
            FireTurboBoost();
    }

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

Oops!

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

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

  • hehe this is a nice little story

  • lol - I enjoyed reading this too.  

    Wish some of my bugs had such a happy ending :)

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

  • 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 :)

  • 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...

  • @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.

  • 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.

  • 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:

    human.trackPos=32004,computer.trackPos=32000

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

  • > @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.

  • 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.

  • 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.  

  • So, the funny story is actually that you never even tested the game before releasing it? :)

  • 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.

  • 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

Page 1 of 2 (23 items) 12
Leave a Comment
  • Please add 3 and 1 and type the answer here:
  • Post