10 ms·
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 bec
by mcraiha 4y ago
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 https://www.youtube.com/watch?v=MytWNbpnAVY
- ReactiveJelly 4y ago> 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_maxhp`. ffmpeg has a whole AVRational class for the same type of problem when converting timestamps accurately between 2 timebases. Reminds me of the TF2 ammo boxes that give 40 metal on Linux and 41 on Windows https://www.youtube.com/watch?v=QzZoo1yAQag&pp=ygUfdGYyIGhlYWx0aCBwYWNrIDQxIGhlYWx0aCBsaW51eA%3D%3D https://www.youtube.com/watch?v=QzZoo1yAQag&pp=ygUfdGYyIGhlY... Apparently the compiler used on Linux chose a less-accurate multiply instruction for 0.2 * 200. Two wrongs make a right - After being less accurate but rounding up where you don't need to, the Linux binary correctly arrives at 40, and the Windows binary arrives at 41. For percents (and dollars) I always do everything in cents and divide by 100 at the very end. "20 * 200 / 100" can be done with pure ints, and f32 can represent i16 exactly.
- zelphirkalt 4y agoSeems all that could have been avoided by using rationals in general and the condition for living being >0 instead of dying <1. Not sure in what case it makes sense to have dying at smaller than 1. To the user all the numbers are integers, so why not use rationals instead of floats? 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.
- DaiPlusPlus 4y ago> 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 values, exactly for things like RTS game unit health, for example. (and Java's lack of operator overloading makes this even more painful). ---- On a related note, can we agree that an application-programming language today should also include signed-and-unsigned "money-safe" decimal types, and IEEE-754 types should be smarter about when NaN can happen - and it should support interval types (and evaluating interval-type-based contract invariants): this would eliminate whole classes of bugs in the first place (too many programmers think single/double is appropriate for storing currency values, ugh).
- PhilipRoman 4y agoAren't rationals subject to exponential blowup in storage/precision? IMO the best approach for business logic values is picking the smallest meaningful unit and representing it with integers. Precision and storage requirements become very easy to reason about.
- DaiPlusPlus 4y ago> IMO the best approach for business logic values is picking the smallest meaningful unit and representing it with integers. How would you design an RTS unit stats system to avoid this AoE Monk HP bug using only integer types?
- bruce343434 4y agoHow much health precision do you need? Make hp an int. Or like a /64 fixed point.
- 4y ago
- thrdbndndn 4y agoCould you explain to a noob why does this happen, exactly? Is it because 55/55 != 1, or 1*1 != 1? I understand float can't store certain integer/finite decimal precisely, but why would a/a not be 1 if both numerator and denominator are the same (imprecise) number?
- zem 4y agoit's because the calculation (1/ 55) is less than the real number 1/55, so when you multiply it by 55 you get a result less than 1. for an easier to follow example, say you can only work with two decimal places, hp was 1, old max hp was 3 and new max hp was also 3. if you do (3 * 1) / 3 you get 3 / 3 = 1 as intended. if instead you do 3 * (1 / 3) you get 3 * 0.33 = 0.99 which is less than 1
- thrdbndndn 4y agoThanks! I misread the equation and thought it's `old_hp * (new_maxhp / old_maxhp)`.
- zelphirkalt 4y agoThis effect becomes more pronounced, when substracting (but probably also adding) small amounts from big amounts, because more bits are needed to store the big number's more significant digits and floats then lose bits for the less significant digits. That is why, if one needs precision, needs to sort numbers first and then start calculating with the smallest numbers first, working ones way up to the bigger numbers.
- pixelfarmer 4y agoKnuth has an entire chapter dedicated to floats, error ranges and such, which includes pretty much that (a+b)+c != a+(b+c) for floats, which in turn breaks tons of things on the math side already. That is such a large issue that it has some serious consequences in the real world, e.g. constructions failing, or I remember the Patriot missile system having problems from that side.
- 4y ago
- layer8 4y ago> So it should have been `(new_maxhp * old_hp) / old_maxhp`. I don't see how that couldn't run into similar problems. I would use `clamp(old_hp * (new_maxhp / old_maxhp), 1, new_maxhp)` (assuming that old_hp >= 1).
- iforgotpassword 4y agoIt could, but only at very large values. It's at least the much more robust solution.
- tpolzer 4y agoIf you assume that all three numbers are small integers, doing the division last minimizes precision loss (and doesn't have any loss if the accurate result is representable as a float). Otherwise you get loss from the division and then multiply that loss (by >1).
- pclmulqdq 4y agoIf you have integers that are stored in floating point types and don't need more than [mantissa] bits of precision, you can operate on them like ints and always* get the right result. Nobody would expect that the formula `return new_maxhp * (old_hp / old_maxhp);` would work with ints, but they see floats as magic that allow them to drop their guard about precision and rounding of operations. Unfortunately not. Rationals have a big problem where under multiplication and division they need to be re-normalized periodically, and that re-normalization is slow (running gcd). Under addition and subtraction, rationals are great. *the one big difference is that division rounding is different
- BeetleB 4y ago> The old "Multiplying and dividing is associative... for the set of reals Psstt... Dividing is not associative for the reals.
- kaslai 4y agoI 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.
- BeetleB 4y agoAh that makes sense.
- filmor 4y agoNone of these would be a problem if people would round instead of truncate by default.
- m463 4y agoI wonder if the fix would be vulnerable to "nudging" hit points up by picking up and dropping a relic? :)
- intrasight 4y agoI woke up in the middle of the night and of course opened up HN. This was near the top and was an interesting read. I told my GF that last night I had insomnia and found myself researching why Aztec monks die when picking up a relic, and that doing so resulted in plumbing the depths of an important computer science topic. She said "Sounds like you were reading Hacker News". She knows me so well now :)