Live data from Hacker News

Aztec monks with 1/55 HP no longer die when picking up or dropping a relic

reddit.com

151–160 of 267 posts

Re: Aztec monks with 1/55 HP no longer die when picking up or dropping a relic

#151
post #2

From the comments: "This was caused by a floating point rounding error. A unit switching jobs internally becomes a different unit (i.e. a monk with a relic becomes a monk without a relic), and when doing that, HP is scaled proportionally based on the old and new max HP using 32-bit floating point maths." And there are YouTube videos about this https://www.youtube.com/watch?v=MytWNbpnAVY

> The formula was probably `return new_maxhp * (old_hp / old_maxhp);` ... > Due to floating point rounding errors, convert_hp(1, 55, 55) equals 0.999999940395355224609375, which is less than 1, which means the unit dies. Ahh. The old "Multiplying and dividing is associative... for the set of reals, not for the subset that you can actually afford to compute with." So it should have been `(new_maxhp * old_hp) / old_max…

> The old "Multiplying and dividing is associative... for the set of reals

Psstt... Dividing is not associative for the reals.

Re: Aztec monks with 1/55 HP no longer die when picking up or dropping a relic

#152

Earlier quoted context omitted.

> The formula was probably `return new_maxhp * (old_hp / old_maxhp);` ... > Due to floating point rounding errors, convert_hp(1, 55, 55) equals 0.999999940395355224609375, which is less than 1, which means the unit dies. Ahh. The old "Multiplying and dividing is associative... for the set of reals, not for the subset that you can actually afford to compute with." So it should have been `(new_maxhp * old_hp) / old_max…

Seems all that could have been avoided by using rationals in general and the condition for living being >0 instead of dying The more I write code, the more I notice how rarely one truly should want floats in most kinds of programs and how they almost always carry a whole bag of problems with them.

Tbh, the only practical non-integers we all need (apart from science-ish pretence and graphics) are .00, .000, .0000 and .000000 decimals (money, qty, percent.00, milliqty). Literally all business and in-game accounting needs get covered by these. Even edge/rounding issues are irrelevant, because everyone is ready to just agree to either way of doing it, and that agreement is much more important than the method itself. This floating bullshit is basically of none, zero, nil practical use outside of academics.

Modern CPUs can effectively emulate whole subroutines in microcode, but somehow we got stuck with that useless binary floating point standard for numbers. And on top of that, languages provide them as a default numeric system for all new software.

Re: Aztec monks with 1/55 HP no longer die when picking up or dropping a relic

#153

Earlier quoted context omitted.

> The formula was probably `return new_maxhp * (old_hp / old_maxhp);` ... > Due to floating point rounding errors, convert_hp(1, 55, 55) equals 0.999999940395355224609375, which is less than 1, which means the unit dies. Ahh. The old "Multiplying and dividing is associative... for the set of reals, not for the subset that you can actually afford to compute with." So it should have been `(new_maxhp * old_hp) / old_max…

Seems all that could have been avoided by using rationals in general and the condition for living being >0 instead of dying The more I write code, the more I notice how rarely one truly should want floats in most kinds of programs and how they almost always carry a whole bag of problems with them.

I dug into this problem a while ago and came to the conclusion that these are all rounding problems, which you still can't escape with arbitrary precision systems, because memory is finite. You're always making a tradeoff between precision and space, and you have to rely on programmers to make that tradeoff themselves.

Enter the typical floating point types that do some of that work already: float/double. These let you not think (as much) about the memory your math might take up, but they still need you to think about rounding, which almost no one does.

This is a long winded way of saying that if you set up your arbitrary precision arithmetic library to use 4 bytes and the same rounding mode (the default is round to nearest) as was used here, you'd reproduce the bug. You'd also reproduce the 64-bit bug moving up sizes, and the 128/256/etc bug. You gotta round.

The good news, though, is that actually solves it! You can even use floats for money!

Re: Aztec monks with 1/55 HP no longer die when picking up or dropping a relic

#154
post #125

Earlier quoted context omitted.

> On a related note, can we agree that an application-programming language today should also include signed-and-unsigned "money-safe" decimal types I’m willing to bet that this has to do with how we historically have handled monetary values which is storing and computing it in the respective currency’s fractional unit. The advantage is that you can still do math operations directly on this value (as opposed to an obj…

> Because we cannot represent fractional units of the already smallest unit of currency, we have to choose a value anyway to charge or dispense. Unlike, say, if we stored it in dollars as a floating point and we can start compounding errors from the floating point type. I've spent 20 years mostly working in finance. You'd be surprised at the prevalence of floating points used to represent currency. I cringe every tim…

You'd be surprised at the prevalence of floating points used to represent currency. I cringe every time

We all cringe but then there’s a “floating point or gtfo” ultimatum that most languages present to you. People would be happy to not use FP. But the reality is, you can have a monetary column in a 30 years old database engine, but not in a 5 years old language runtime. When you only have a hammer…

Re: Aztec monks with 1/55 HP no longer die when picking up or dropping a relic

#155

Earlier quoted context omitted.

> The formula was probably `return new_maxhp * (old_hp / old_maxhp);` ... > Due to floating point rounding errors, convert_hp(1, 55, 55) equals 0.999999940395355224609375, which is less than 1, which means the unit dies. Ahh. The old "Multiplying and dividing is associative... for the set of reals, not for the subset that you can actually afford to compute with." So it should have been `(new_maxhp * old_hp) / old_max…

> The old "Multiplying and dividing is associative... for the set of reals Psstt... Dividing is not associative for the reals.

I think the original intent was referring to the fact that given the same operand, the order of multiplication and division shouldn't matter, e.g. A * X / X should give the same result as A / X * X, but in reality they can give different results when done by a computer due to precision limits, overflow, etc.

Re: Aztec monks with 1/55 HP no longer die when picking up or dropping a relic

#156
post #149
post #140

Earlier quoted context omitted.

> A better solution is to kill the unit when it drops below zero HP Then you have the situation where units are displayed as having 0 HP but are still alive.

So what? The players already know that attacks deal fractional HP damage.

> The players already know that attacks deal fractional HP damage.

Top-end players who analyze the engine enough do.

Otherwise, they shrug and say "sometimes 1, sometimes 2 HP"

And even so, we're used to seeing 0/50HP and it meaning "dead" across many games. Seeing 0/50HP and it meaning "just a small amount of HP left" isn't ideal.

Not to mention that weird situations with 5.551115123125783e-17 HP remaining aren't great either (where a player may end up with effectively an "extra" hit point).

Re: Aztec monks with 1/55 HP no longer die when picking up or dropping a relic

#157
post #142

Earlier quoted context omitted.

> The best solution is probably to store the current HP as a percentage of the max, because then you never have to rescale it in the first place. Doesn't this solution make the more common calculations more expensive, complicated, and error-prone to make one rare calculation easier? It seems like far more of the operations on the unit's current HP will be "gets damaged by X hp" or "gets healed by X hp", both of which…

If you’ve changed HP to a percentage, why wouldn’t you also change the damage and other related numbers (such as damage reduction and so on) to percentages as well? Nobody would be dumb enough to store them in different units and then convert every time they manipulate them.

> why wouldn’t you also change the damage and other related numbers (such as damage reduction and so on) to percentages as well?

Because a sword might do 5HP of damage, not 10% of anyone's damage (whether they've got 50 or 5000HP). Otherwise, hit points are meaningless: 10 attacks with a 10%-damage weapon kills a lowly serf or a mighty dragon.

And because damage reduction might reduce a fixed amount of damage from an attack. Etc.

Re: Aztec monks with 1/55 HP no longer die when picking up or dropping a relic

#158

Earlier quoted context omitted.

> Seems all that could have been avoided by using rationals in general It's 2023: I'm still surprised that so many new languages/platforms still only provide only basic integer and IEEE-754 types for numerics: Rust, C#, Java, Swift and others still lack built-in, runtime-provided, nor even standard-library-provided rational types, which I assume really should be the preferred type for most business/domain/application…

> It's 2023: I'm still surprised that so many new languages/platforms […] still lack built-in, runtime-provided, nor even standard-library-provided rational types Because numerators and denominators can blow up easily, rational types effectively require storing them as bigints. That makes them bad from a performance viewpoint. Assuming your application will work fine with fixed-size rationals (which, IMO, is highly u…

> Because numerators and denominators can blow up easily, rational types effectively require storing them as bigints. That makes them bad from a performance viewpoint.

While hardware support for full number towers might be neat, I don't think anyone is suggesting that int and floats should not have language/standard library support - only that there should be a blessed option for precise arithmetic.

Usually the slow correct answer is preferable to the quick wrong answer (esp in business logic).

Re: Aztec monks with 1/55 HP no longer die when picking up or dropping a relic

#159

Earlier quoted context omitted.

> The formula was probably `return new_maxhp * (old_hp / old_maxhp);` ... > Due to floating point rounding errors, convert_hp(1, 55, 55) equals 0.999999940395355224609375, which is less than 1, which means the unit dies. Ahh. The old "Multiplying and dividing is associative... for the set of reals, not for the subset that you can actually afford to compute with." So it should have been `(new_maxhp * old_hp) / old_max…

Seems all that could have been avoided by using rationals in general and the condition for living being >0 instead of dying The more I write code, the more I notice how rarely one truly should want floats in most kinds of programs and how they almost always carry a whole bag of problems with them.

  bool is_alive(const Entity* unit) {
    return unit->hp > ZERO_FLOAT;
  }
Super easy…

Likewise I’m with you, the more I code, the more I see these kinds of things as bad design choices. If you’re going to deal with a type, keep it that type. If you’re going to convert, you need to take care of edge cases like OP. And don’t just add 0.0001f; that’s an exploit waiting to happen by someone with a macro to pickup/drop relics.

Post reply on HN