Live data from Hacker News

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

reddit.com

101–110 of 267 posts

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

#101
The thing I like the most about this is how the game actually switches out a unit with a different unit depending on the task the unit needs to do. It's not exactly in the reddit post, but rather in the video https://www.youtube.com/watch?v=MytWNbpnAVY where the error is outlined. What a brilliant way to keep complexity down.

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

#103
post #68

Earlier quoted context omitted.

Did this exponential blowup ever prove troublesome in practice? I think it can be easily solved by strategically placed rounding steps. But explicit rounding is advisable in many cases anyway.

But strategically placed rounding was in fact the solution to the original problem that in this thread Rationals were proposed as solving... Rational types are not super popular and don't get used that much, most (not all) people that end up using them do it very intentionally and consciously and know what they're dealing with -- if they were more ubiquitious, I suspect more trouble would be seen "in practice". While…

Or it's vendors that don't care about correctness, like providing decimals by default and high performance floating point as an "optimized" option. Python did something like that by using bigints as a default for integers.

What would not have been true 30 years ago? I remember "real" fixed-point type built-in in Turbo Pascal and explicitly documented as suitable for money. Common Lisp has had first class fractions support since the start.

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

#104

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.

> Seems all that could have been avoided by using rationals in general

Why can’t they store HP internally as “millihealth” integers, do integer arithmetic (including division), and then divide that by 1000 purely for display?

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

#105

Earlier quoted context omitted.

Rationals fail in their own spectacular ways. Take a simple approximate exponential decay: x ← (99/100) * x. In exact rational arithmetic, your program will happily calculate integers (99^k) and (100^k) for unboundedly large values of k (there's no reduction – they're all coprime), grinding the program to a halt until it runs out of memory. "Oh, the solution is obvious: you should simply round that to an approximate…

The difference is, that with rationals you do not lose any precision on the way. I doubt, that 99^k is part of AoE2 code. For the simple calculations in AoE2 regarding unit health, rationals will do a fine job. With rationals the rounding does not happen during the precise calculation. It happens explicitly, only once, at the end of the calculation, at the programmer's wish and conscious decision, thereby avoiding th…

It's a very common pattern, in its more general forms. Sure, you can abstract away the naive re-implementation of exp()

    loop { x ← a*x } 
as one function call x(t) = exp(-k*t), so that one looks pointless. But moving-average [0] type expressions

    loop { x ← a*x + f(t) } 
are more interesting. You can't generally refactor those into exp() calls, like x(t) = sum[ f(t_i) * exp(-k*(t - t_i)) ], since that introduces a memory leak – you need to hold an explicit list of all the past values of f(t), in order to compute it this way. That's why the pattern [0] is uniquely useful: it's a trick to incrementally accumulate a moving average using just one variable of state.

[0] https://en.wikipedia.org/wiki/Moving_average#Exponential_mov...

Here's one uncontrived way this pattern might show up: "the NPC has a numeric disposition towards the player, and the player's actions have positive and negative effects on it. These effects have finite duration, and disposition decays back to the neutral value". Another way: you have a large list (like a time series) you want to reduce to a small one by interpolation. Another way: you have some PID-like controller in your program, and to make it behave, you need a cheap filter to clean a jittery input signal. (I'm building these for my Factorio factories right now – it's the only reason this example occurred to me. "x ← (99/100) * x" is a factory control component I had to implement out of int32_t's).

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

#106

Fun fact: The competitive AoE2 scene remains very much alive and thriving. Several top players are able to make a living from it, thanks to streaming revenue and tournament prize money. There are even some streamers, such as T90 and MembTV, that have managed to create successful careers despite not being competitive players themselves.

Note that T90 is like 2300 Elo. Which isn't "top level pro", but I'm pretty sure he'd kick most of our asses. T90 is probably the strongest dedicated commentator / announcer / caster in this community.

tbh I credit ZeroEmpires with keeping the community alive through the slower times though.

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

#107
post #103

Earlier quoted context omitted.

But strategically placed rounding was in fact the solution to the original problem that in this thread Rationals were proposed as solving... Rational types are not super popular and don't get used that much, most (not all) people that end up using them do it very intentionally and consciously and know what they're dealing with -- if they were more ubiquitious, I suspect more trouble would be seen "in practice". While…

Or it's vendors that don't care about correctness, like providing decimals by default and high performance floating point as an "optimized" option. Python did something like that by using bigints as a default for integers. What would not have been true 30 years ago? I remember "real" fixed-point type built-in in Turbo Pascal and explicitly documented as suitable for money. Common Lisp has had first class fractions su…

I meant to suggest that 30 years ago the performance difference between "floats" (floating-point binary) and "BigDecimal"-style arbitrary-precision floating-point decimal would have been much more significant to many more real-world use cases, compared to now. So that may have been a reason not to make them the default when you simply write a literal `5.4` in code, but that argument is less now.

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

#108
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…

If 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

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

#109

Earlier quoted context omitted.

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.

> 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…

I'm increasingly convinced that peoples' problems with 754 types are psychological. With integers, they are very careful about order of operations, precision, rounding, etc. With doubles, the average programmer fires and forgets. Sometimes that has bad consequences.

A decimal type is definitely necessary, and you can use IEEE decimal floating point for that, too! Other decimal types are very useful. Rationals are a different story: rational numbers are great for addition and subtraction, but if you're going to be doing a lot of multiplication and division, you are going to find yourself also doing a lot of slow gcd calculations to reduce/renormalize the fractions (incidentally, decimal types also need renormalization, but it's a lot cheaper). Most cases where rational types are used today are very careful about not having long chains of these operations.

As to NaNs: those are all considered pretty carefully, and 754-2019 actually reduced the amount of NaNs that propagate around quite a bit. For example, make sure you are using fmax(a, b) instead of std::max(a, b).

Post reply on HN