Hacker News new | past | comments | ask | show | jobs | submit login
Aztec monks with 1/55 HP no longer die when picking up or dropping a relic (reddit.com)
525 points by danso on April 15, 2023 | hide | past | favorite | 208 comments



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_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=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.


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


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


> 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 unlikely), the natural way to store rationals in fixed-size storage wastes lots of room.

For example, in a (int32 nominator, int32 denominator) pair, there are about 2³² ways to encode ‘1’ that way, 2³¹ to encode ½, etc. I also think such a fixed-size rational type isn’t very natural to work with

Rationals also require regularly computing a gcd when you add (1/3 + 1/6 isn’t 9/18 but 1/2) or multiply (10/21 × 7/5 isn’t 70/105 but 2/3) them, slowing down things more (you can use heuristics to avoid some of those simplifications, but not doing one when that’s possible may mean your rationals keep huge numerators and denominators, slowing down operations)


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


I'm (vaguely) surprised there hasn't been a hardware type for rationals. Then the GCD operation could be done in hardware and everything would be fast (at least as fast as floats). Not surprising, because adding new hardware data types is hard, but the financial institutions would benefit from something like that.

A rat64 could be:

- 1 sign bit

- 3 format bits. Determines where the decimal is located, e.g. 30.30 vs 52.8.

- 60 bits of numerator and denominator. The split is determined by the format bit. 50.10 would be handy for any percent/ppt operations (e.g. Most USD operations)

Might need an error bit, but that's my 5 minute gist.


> Then the GCD operation could be done in hardware and everything would be fast (at least as fast as floats).

Could it? Is there a fast way to do GCD in hardware?

Googling gave me https://en.wikipedia.org/wiki/Binary_GCD_algorithm, which needs O(log₂(max(u, v))) operations to compute gcd(u,v), but I wouldn’t know whether that’s the best we can do in hardware, or whether that’s ‘as fast as floats’.

Also, I don’t see how your format describes a rational type. Rationals store numbers as p/q for integer p and q, not with a decimal point.


Sorry, abuse of notation. 30.30 would be 30 bits numerator, 30 bits denominator.

I would imagine it would not need to fully reduce after every operation, there's probably tricks where if you know the factors going in, there will be obvious ones after multiplication/division.

It's not my best idea :p


> 60 bits of numerator and denominator.

The trouble is this isn't enough, even for ordinary usage. The numerator and denominator grow in proportion to the total number of operations you have performed. For example, if you start with `1` and then multiply by `80/81` a thousand times, you get a number that's around 1e-6, but when expressed as a rational the numerator and denominator have hundreds of digits:

https://www.wolframalpha.com/input?i=%2880%2F81%29%5E1000


But how often are people using decimal types to do that? Most of the uses I could see this type being used for - currency, percent scaling, datetimes, audio/video codec frame rates - all are basically fixed point operations. Anything involving powers of 80/81 would probably need bigint based rationals anyways.

Actually if you had an int64 type which was scaled by flicks, that'd give you quite a lot of latitude for most day to day stuff.

https://en.m.wikipedia.org/wiki/Flick_(time)


Yeah, fixed point is different from rational, and all those examples you gave sound to me like fixed point. And that can be implemented efficiently without dedicated hardware support: the denominator is fixed, and you store the numerator as an integer.

A 1/3 off discount on a $10 item is $6.67 (or $6.66 if rounding in the customer's favor), not $10/3.

(Except datetime, did you mean timestamp? A timestamp is an instant in time, and it often makes sense to store it in high precision because you're saying exactly when something happened. A datetime is for communicating between humans who are using some particular calendar; it rarely makes sense to have more than minute precision.)


Aren'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.


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


`(new_maxhp * old_hp) / old_maxhp` avoids the original problem when using integers. Though you still need to make sure your type is big enough to not overflow.

Or using fixed-point (which are a relatively simple abstraction over integers): `old_hp * (new_maxhp * old_maxhp)`.


They probably just added: return ROUND()... and went back to play


How much health precision do you need? Make hp an int. Or like a /64 fixed point.


Congratulations, you just introduced a new damage threshold / resistance threshold / etc where things effectively do no damage.

Fixed point would not have helped here. Integer would only have helped because dividing first would break immediately.


     max(1,...


Well, in this case, if a unit is converted to a different type and back, it would just keep its (absolute) value of HP (in this case 1), no need to scale anything.


You haven’t thought it through yet. In this case, HP _is_ an integer, and so is the MaxHP for the unit type. And yet the bug still occurs! Storing the HP as an integer does not prevent the problem from ever occurring, because fundamentally the user still expects HP to be proportional.

That is, suppose a unit is sitting there at full health, and you research a tech that increases that unit’s max hp? Should the unit now be at less than full health, as if it had been in combat? Users probably don’t like that.

Suppose it is at full health, and then it gets downgraded to a type with less maximum health. Does it stay at it’s current HP? Users probably won’t like being attacked with supercharged units that have extra HP; they’ll think that the other player was cheating somehow.

What if it is damaged and at half health, then gets upgraded. Should it be fully healed? Gain HP equal to the difference between the new and old maximum HP? Or should it gain half of that, so that it stays at half health?

Or perhaps HP is too limiting, and the game should do what Dwarf Fortress does. DF knows the approximate surface area of every body part (based on each creature’s body plan, plus individual stats such as strength, fatness, size, etc), and the size of every weapon. Every attack therefore deals damage to a certain area, measured in square inches, and individual body parts will be destroyed once a sufficient percentage of that surface area is damaged by wounds.

Or maybe you are designing this game in the 90’s, and you have to worry about squeezing unit updates for 200 units per player into the bandwidth provided by the average modem of the day (probably 28.8kbaud), so you stick to one integer because it’s the simplest think that can possibly work, and the number of bytes per update can be calculated in advance. And then some other schmuck gets stuck with the job of handling unit type changes (but be warned that the schmuck might be yourself in six months).


>That is, suppose a unit is sitting there at full health, and you research a tech that increases that unit’s max hp? Should the unit now be at less than full health, as if it had been in combat? Users probably don’t like that.

>Suppose it is at full health, and then it gets downgraded to a type with less maximum health. Does it stay at it’s current HP? Users probably won’t like being attacked with supercharged units that have extra HP; they’ll think that the other player was cheating somehow.

>What if it is damaged and at half health, then gets upgraded. Should it be fully healed? Gain HP equal to the difference between the new and old maximum HP? Or should it gain half of that, so that it stays at half health?

     if (oldHP == maxOldHP) { newHP = newMaxHP }
     else { newHP = max(1,scale(oldHP,oldMaxHP,newMaxHP))
not exactly complex


Sure, if you fully predict all bugs in advance and compensate for them, nothing is complex, but that's not really feasible.


>Sure, if you fully predict all bugs in advance and compensate for them, nothing is complex, but that's not really feasible.

Uhh, last time I checked, that was literally my job, and specifically what I went to school for!


Not exactly complex, but you posted too quickly to have found the best solution.

A better solution is to kill the unit when it drops below zero HP, not when it drops below 1.0; scaling will never move the current HP past zero in either direction. Having 0.9999994 HP instead of 1.0 HP would not cause any problems then; the unit is still one hit away from death in either case.

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.


> 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 would require the equivalent of the above conversion to establish a result.


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.


Obviously a dragon doesn’t get damaged at all by a puny little sword. You can’t just say that hitting a dragon with a sword a thousand times would kill it, when none of the attacks can get through the dragon’s armored hide. On the other hand, it will die immediately if pierced by an arrow provided that arrow hits the one spot where the hide is missing a scale. The dragon has 100% DR, except in that one spot.


> Obviously a dragon doesn’t get damaged at all by a puny little sword. You can’t just say that hitting a dragon with a sword a thousand times would kill it, when none of the attacks can get through the dragon’s armored hide.

https://www.youtube.com/watch?v=9VDvgL58h_Y


:D


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


Again I say “so what?”. The unit is one hit away from death either way, and the player will know it.


At this point, with your Smaug-related comment, etc-- I've become convinced you're trolling.

But just to humor you: the desire is to have a system where floating point neither surprisingly kills nor gives characters extra hit points. There's a lot of subtlety in this. Schemes where all damage are percents don't preserve the essential pieces of RPG combat systems. Moving the threshold from 1 to 0 HP violates the conventions of the art and doesn't eliminate the problems (you can still end up with hit points epsilon away from 0, instead of 1).


The problem isn’t hitpoints that are an epsilon away from 1, it’s that an operation which looked innocuous moved the HP to an epsilon _below_ 1. That would never happen with 0, however, as multiplication and division of positive numbers always gives a positive result.

And in D&D, incapacitation happens when a character drops below 0 HP, and death happens once they drop below -10HP. There is no grand convention that all RPG games follow here; each game is making things up as they go. They picked badly when they designed Age of Empires, and that’s ok. It’s not the end of the world. Just know that we can do better if we are paying attention.

And I am serious when I ask “so what?”. Why is it so bad if the game displays 0/55HP? The players all know that fractional HP damage exists, and they can see that their unit is still alive, so what is the problem? They will be at least as amazed that their unit survived as they currently are when they see a unit at 1/55HP.

I am likewise serious about Smaug; there is no way that combat makes any sense if Smaug has a large number of hitpoints or that any weapon at all can damage him. Using hitpoints as an abstraction completely loses all of the flavor of Tolkien’s original descriptions. A better model is one using damage reduction and sensible internal organs; a strike that pierces the heart will kill even a dragon.


> however, as multiplication and division of positive numbers always gives a positive result.

Depends on what you do with subnormals and underflow, etc.

> There is no grand convention that all RPG games follow here

Computer RPGs have shown 0/812 for dead for ... forever.

> The players all know that fractional HP damage exists

I think most players will be confused. And for what, to make it "safe" doing floating point operations in the wrong order? (I doubt it's still safe, overall).

> I am likewise serious about Smaug; there is no way that combat makes any sense if Smaug has a large number of hitpoints or that any weapon at all can damage him.

Common mechanisms: an armor class that makes it hard to hit; a fixed amount of damage reduction so that a needle does nothing; a lot of hitpoints.

> and sensible internal organs;

It gets excessively complicated and annoying. Battletech is already excessively complicated, and doesn't go quite as far as you're advocating for here.


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 it's probably true that any digital number format will have edge cases that in fact come up in practice, my personal choice of "Why the heck isn't this more popular, why doesn't every language support it, why isn't it in fact the default representation of a numeric literal" -- is floating point "decimal" types, like ruby (or I think Java?) BigDecimal, rather than rational. I think they mostly work matching programmer's mental models of numbers, and for many/most common uses on 2023 platforms the performance is just fine. (this would not have been true 30 years ago).


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.


Slight correction, from a former Turbo Pascal (and subsequent Delphi) programmer...

"Real" types were platform-dependent floating-point types, not suitable for monetary calculations whatsoever. and would map to either Single or Double depending on the underlying CPU architecture. Sort of like a C-style "int" that would map to an 8-bit, 16-bit, 24-bit, 32-bit, 36-bit, 60-bit, 64-bit etc integer, depending on the compiler and the compile target.

Turbo Pascal did have an 8-byte, fixed point, "Currency" type suitable for monetary calculations, however using it was very, very slow compared to pure floating point ops - just as the comment you replied to suggested. If that weren't enough, library support (both built-in and 3rd-party) for math and other utility functions was either limited or non-existent.


Slight correction from an Turbo Pascal expert ;-)

Turbo Pascal did NOT have an 8-byte fixed point "currency" type suitable for monetary calculations. It did have an int64 type called "comp" though which was handled as a float by the FPU and hence not slower than the types "single" (f32), "double" (f64) or "extended" (f80).

IIRC, "currency" came with Delphi V2.0 (or even later), but then still it wasn't slower than other floats when you did heavy calculations with it as it was also handled by the FPU. Only reads and writes from and to such variables were expensive as there was always a scaling (multiplication by 1e4 and 1e-4) involved — internally it was that int64 "comp" type. (But here I might be wrong, I never really used "currency", I disassembled lots of Delphi binaries with lots of different data types as I wanted to know how the compiler worked. Today however my Delphi knowledge is quite fuzzy).


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.


Fun fact many processors actually support decimal types in hardware. I believe you can use them in c with _Decimal. Worked on my Ryzen 3700x and (if memory serves) my M1 mac


Due to the downvotes I looked into this again. You can use _Decimal32 etc in GCC on x86, but it is a soft implementation. I believe it is in hardware on some ibm platform, but definitely not on the M1


>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

C# has the base-10 high accuracy decimal type: https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...


There are two problems with the `decimal` type: it's not IEEE-754 compliant, and it is 16 bytes, which prevents atomic reads/writes (without locks).

This is a problem in one of my company's applications, as we have a background thread doing work entirely with `decimal` objects, but providing a readout for the GUI necessitates a lock on every write (even if never read), and another for the read by the GUI. There's barely any overhead (nanoseconds worth), but the logic to work around it is a pain, and, if you forget to actually use a lock, good luck with that bug.

.NET 8, however, will introduce[0][1] proper IEEE-754 decimal float types, including `Decimal32` and `Decimal64` which will allow atomic reads/writes.

[0]: https://github.com/dotnet/runtime/issues/81376

[1]: https://github.com/dotnet/runtime/issues/79004


> can we agree that an application-programming language today should also include signed-and-unsigned "money-safe" decimal types

You mean like COBOL?


Yep, like COBOL, or SQL.

But, in fact, I don't know of any language that doesn't. They are just not basic types.


Very few SQL implementations have unsigned integers (I think only MySQL+MariaDB has it out-of-the-box?). From my conversations with the MS SQL Server dev team, other than the overall lack-of-demand, it’s because ISO SQL leans very heavily on implicit conversions, and implicit conversions between signed and unsigned types are a massive pain to implement and gracefully handle all edge-cases and in a way that doesn’t behave too unexpectedly (and remember that most SQL language users are likely unfamiliar with all the gotchas of C-style unsigned integers: most SQL users are business analysts, not low-level systems programmers). Besides, if you just want to disallow negative values then just use a CHECK constraint.

———-

Also, in general, it’s not enough to just add loads of distinct numeric types to a language or library; in fact that’s probably the wrong thing to do as it burdens the programmer with having to make (often hair-splitting) decisions ahead-of-time - instead I’d like the language to have me set-up high-level constraints/invariants on a numeric parameter, local, or field and then the compiler chooses the best low-level implementation based on those constraints (and constraint-inference a-la Hindley–Milner would be nice too)


ClickHouse has support for unsigned integer types: UInt8, UInt16, UInt32, UInt64, UInt128, and UInt256.

The main applications are: hashes and identifiers, e.g., visitor identifier in clickstream.


> 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 object that contains it) and at most you’ll be off by, in the case of the US dollar, one cent. 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.

What interesting is that pre computers, we didn’t even treat currency values as an real decimal (for base 10 currencies) and the decimal point was simply a convenient way to store partial values. I note this because you don’t see old ledgers where they store more than 2 decimal places, therefore IMO, this was just an integer in disguise all along.

Old habits die hard?


> 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 time I see it, but it's surprisingly common (and wrong).

More correctly, pricing of securities (from exchanges) is done with integers and a scaling factor. The factor is typically static and doesn't need to be transmitted on every tick. The factor tells you effectively how many fractional digits are present (e.g. the actual price is multiplied by 10^-factor). Factors of 4 or even 6 are somewhat common, but some securities have to go with less precision. I remember in particular Berkshire Hathaway causing issues with overflow using 32-bit ints in the last decade (because they never split their shares, the share price is quite large).


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…


In that situation you’re meant to use integer-cents though.


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<double>(a, b).


> It's 2023

This is a game originally from 1999. Although i doubt it would make a difference.


Java pretty much provides BigDecimal for that


A related discussion about number types:

The Great Debate @ARITH23: John Gustafson and William Kahan

https://www.youtube.com/watch?v=LZAeZBVAzVw

Kahan points out disadvantages of interval maths.


why would rationals be good for RTS.

If all of your operations occur with some reasonable minimum denominator, just use ints. If not, that arithmetic is going to become unbounded really fast.


Foundation has the Decimal type (not technically part of the Swift standard lib, but in practice available for most uses of Swift).


> and Java's lack of operator overloading makes this even more painful

Why?


Some aspects of the problem, exagerated for effect: boolean isDead = (new I("0.9")).divide(new I("55")).multiply(new I("0.1")).lessThan(new I("1"))


Because you can't make your own types that work better.


Of course you can. You just can't use operators for them.

Unless you go Haskell where anything can be an operator name, operators are just a minor convenience hack .


So you can but you need to rewrite everything instead of just changing a type, and the code will look completely impossible to read.


And now all your simple scalar values need to exist on the heap as class object instances - and will incur plenty of runtime allocation costs for every arithmetic operation - or try to risk it with non-immutable heap-allocated objects, but that’s a recipe for disaster: no-one should ever think mutable shared state is a good first-design.

A lot of Java/JVM’s design decisions made sense in the mid-1990s, such as not supporting user-defined stack-allocated value-types and only supporting GC-managed objects - as main-memory was fast-enough compared to CPU registers - and many/most Java targets didn’t have any kind of CPU data cache - the idea of using the heap for basically everything wasn’t a bad design… back then. But now it is: high-perf code for the CLR means eliminating unnecessary GC allocation wherever possible - but in the JVM that isn’t possible.


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 fraction..." — yes, and that's floating-point arithmetic. Point is, you can't use either with thinking, about numerical representations and their possible failure modes.


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 the kind of issue we see here.

Rationals might not be the solve all problems type, but here they seem very applicable.


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


In a simulation nobody is calculating n x m^k. They’re calculating n X m in k loops spread over k ticks. And given that fact, if you round or truncate at the end of each cycle, as one often does with game points (eg, fire resistance in D&D), then there is no problem.

For money systems, we also tend to round on every step to the nearest penny.

What you don’t have with rationals is the problem of 10.00 - 4.10 + 3.20 ending in a 9.


The trouble you have is that you really wanted reals and the rationals are not a much better substitute than the floats (which are mostly just one particularly specialised flavour of rational)

It doesn't feel immediately like there ought to be a problem, but witness, mathematically almost all of the reals are Normal and unfortunately Normal doesn't mean "normal" it's a technical mathematical word, that for our purposes means roughly "completely batshit".

Normal numbers are non-computable, so we definitely don't want to try to use them in software, which is a problem because again, almost all of the reals are Normal.


Huh? This isn't making a lot of sense to me. Why do you say the rationals aren't much better then floats? In situations like this surely we absolutely want to be working in the rationals, and they're a very easy way to do these calculations correctly without having to worry about tolerance bounds for arbitrary-precision float calculations


Algebraics plus pi avoid almost all the concern you mention, but still have the same problem for software. All those nasty reals are unreachable by your program.

The actual problems with irrationals is that it is extremely hard to measure their size without converting to rational approximation, so you are back to the original problem.


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

This! Imho you more or less never want floats under "normal" conditions.

Also in most application domains it makes exactly no difference that floats have HW support, as the bottlenecks are there usually elsewhere. And in case you really need real fast floating point math (say for simulations) you would do it anyway on the GPU nowadays.

The only langue I know of that is sane in this regard is Pyret:

https://www.pyret.org/

[CTRL-F: Pyret has numbers]

I'm still baffled no other new language thought about something so basic like number ever again. Everybody is just using what the HW provides natively since the year of yore. As a result there is no motivation for HW vendors to update the status quo form the state of the art of the 60's of the last century. Imho having support for rationals and (fixpoint) decimal numbers in std. hardware is long overdue! But it would only happen if there would be serous demand form the runtime / language vendors. Classical chicken egg problem.


Floats are a computer representation of scientific notation, used for scientific computation. Coincidentally, hardware support is extremely important for scientific computation. (A bit less now than in the 90's but still a showstopper.)

Hardware support is also extremely important for games, but floats are not a good representation for most of it. IMO, game data should be composed exclusively of integral numbers, but it is natural to want a little bit more of precision some times.


> Floats are a computer representation of scientific notation

No, they aren't. They are IEEE 754 binary floating point data. Something extremely weird, without precedent outside of computers science.

> Coincidentally, hardware support is extremely important for scientific computation.

Maybe it was once, but today it isn't.

If you need to do any serious number crunching you use nowadays dedicated hardware for that (which isn't part of the main CPU usually). (You could use CPU integrated GPUs or FPGAs for that, sure, but the point is that the FP unit on the CPU is usually way to slow for anything more serious.)

> Hardware support is also extremely important for games

That's more or less the only valid usage in mainstream left by now. But even there:

> but floats are not a good representation for most of it.

Exactly!

As said, you could and should use ints for most things. For the rest you want actually rationals. Only in some very special circumstances floats are a good enough approximation. But the cases where this is true are mostly related to rendering, as it doesn't matter if some pixels shown for the fraction of a second have a marginally wrong color or are marginally off. But rendering is done on the GPU anyway. So again no reason to use FP features on the CPU.

Even games using vector and quaternion math extensively this is just lib code in the engine. So there is no real reason this couldn't be moved to the GPU also, given an adequate architecture of the game engine. Such an approach brings even amazing possibilities for games; just have a look at for example https://store.steampowered.com/app/1468720/Ultimate_Epic_Bat... Want to animate 10 million of individual NPCs? No problem, if you do it on the GPU!

So imho the case for FP on the CPU is very shallow. But the need for rationals and decimals is a real thing. That's what the average cooperate developer needs day to day.

It's a shame HW is stuck in the past since decades and there is still no promise of progress on the horizon. (Maybe FPGA accelerators build into CPUs will change that at some point. But we still don't have that, even it's overdue.)


I think most (all due to standard?) Scheme dialects have rationals. Pyret seems to have inherited them from Racket, which inherited them from Scheme. Common Lisp also seems to have them (I just tried (/ 1 3) in SBCL REPL.). So there is quite a number of languages which have rationals, but many mainstreamy ones miss out in this aspect.


Scheme got rationals from Common Lisp. See R2RS.


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!


The big problem with rationals is that it's easy to end up with both nominator and denominator growing without bounds, which IMO is an even bigger pitfall than those of floating-point or fixed-point.


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


This would require competent senior programmers who are way too expensive, and anyways once hired are used for bullshit tasks like project management.


You mean fixed point numbers presumably, because at the time using integers that use a variable amount of memory was totally out of the question. Today it’s still terrible for performance because you can’t use caches and simd effectively when numbers can’t be packed directly in arrays.


Does AoE2 do that? Packing numbers directly into arrays? Or does it rather deal with objects, which are variable size anyway? I am not sure how much high performance work has been done in AoE2. Maybe not that much.


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

If killing unit takes X attacks, user buys 10% attack damage upgrade, and it still takes X attacks they will be annoyed. If rounding down takes that to x-1 attacks it might be preferable.

That being said you're absolutely right. just use HP*100 in calculations and display HP/100 to the user, and when you need to round into specific direction do it explicitly


  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.


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.


In terms of hp without decimal in gaming and related with max hp changing, the best solution should be to rounding down (or nearest zero rounding) the result, and keep minimum hp to 1 when max hp changes. In case for minimum hp unit (1 in this case) to have changed into lower max hp then they'll die too, hence why minimum 1 hp as the result is logical.

This is exactly why heroes won't die in Dota2 when toggling off armlet while having 1 hp


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


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


Ah that makes sense.


Could 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?


it'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


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


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


Thanks!

I misread the equation and thought it's `old_hp * (new_maxhp / old_maxhp)`.


It's neither. Diving 1 by 55 gives some number x where x is arbitrarily (there are standards for how to choose) chosen between the closest 2 representable floats to 1/55. Then, they multiply x by 55. If x is bigger than 1/55 here then the result is > 1. But, if it was rounded down then it will fail because x * 55 will be less than 1.


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


If 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).


It could, but only at very large values. It's at least the much more robust solution.


None of these would be a problem if people would round instead of truncate by default.


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


I wonder if the fix would be vulnerable to "nudging" hit points up by picking up and dropping a relic? :)


It’s great that Microsoft are still actively maintaining a ~24 year old game!

Blizzard ought to take note. A few years back they announced “Warcraft 3 reforged” with great fanfare: a remastered version of the classic Warcraft 3, updated for modern systems with improved graphics etc. It looked/sounded fantastic!

But despite it being a paid upgrade, what they released was a buggy mess that did little justice to the classic original. Then they immediately abandoned it and have released no updates or fixes since.


It's actually worse, they pulled the legacy version from the users and forced the (effectively) downgraded version to everyone. Even the owners of the legacy version were shafted as they forced a update which removed much of the functionality of the legacy version. To this date still the best version was the pre-patch one for everyone who has the original installers and the latest pre-upgrade patch


And the AoE2 DE has way higher system requirements while only looking marginally better in some aspects, but looks worse than 2 decades old 2d art in some other aspects. Just because it "has to be 3D" these days, while there is nothing truly requiring any 3D in the game.

This is subjective, but I liked some of the old unit designs more than the new ones. For example Teutonic Knights and Cataphracts looked better in my opinion. The Teutonic Knight looked leaner and taller, while it now looks a bit like a walking barrel. OK, somewhere they need to carry all that armor ... but still I liked the old ones better.

Also I would have been completely fine with units having only 8 or whatever directions in which they can look. I don't need 360° all-around rendering of units. It even substracts a little from AoE2's abstract character.

What I do like is, that they added more civs.


I think DE actually looks worse. Old graphics hold up great - just look at this: https://imgur.com/hWSZBpb. The new ones lost the soul. They have a weird glow/fuzziness. Nonetheless, DE has better multiplayer performance and cool new civs, so it's the one I play.


> I don't need 360° all-around rendering of unit

But you don't have it anyway? I assumed the game is still actually isometric and not 3d but even if it is the camera is still fixed.


Update: In fairness to Blizzard, I did just check this again and it seems like they have started maintaining Warcraft 3 Reforged again, releasing some small patches last year and a significant update in January 2023:

https://news.blizzard.com/en-gb/warcraft3/23896791/warcraft-...

I'll have to take another look now that this has been released.

But still, there was a 2 year stretch between 2020 and 2022 when the game was in a very crappy state and absolutely nothing was done to improve it or address the issues.


Blizzard also pulled the original W3 downloads and switched everyone to a non-hd version of Reforged. I'm still mad about that because I paid for both and I'm no longer able to get one of them.


But isn’t Reforged using the same engine as the original, with higher res assets (mostly textures)? At least, I thought so, as that was part of the criticism of Reforged.


Nothing wrong with that, necessarily. In fact, I’d argue that’s exactly what I would want out of an ideal War3 reforged. Exactly the same game modernised for modern systems, with the “same” graphics just at higher resolution and detail.

Besides, World of Warcraft, StarCraft 2, and Heroes of the Storm were all built using upgraded versions of the Warcraft 3 engine, so it’s proven to be quite capable. It’s logical to use an updated version of the engine for an updated version of the same game.

Trying who port the game to an entirely different engine would not only be much more effort, it would risk introducing subtle differences in gameplay and feel that could mean it doesn’t play quite the same any more.


I haven't played it for a while now, but one of the major complaints after release was that many of the old custom maps didn't work.


I'm surprised this isn't illegal.


I would imagine that in certain countries with strong consumer rights (e.g. Australia or Germany) it may well be, just understandably no one bothers with the expensive legal fight it would take.


This kind of crap is actually why I will not buy Diablo 4 (even though I kinda want to, to have a good hack'n'slash on my PS5) or put money into any Blizzard game anymore.


The amazing thing is that this new version of AoE was a fan made upgrade, and they did such a good job that MS brought them in house.

The Forgotten Empires team is brilliant and I can't be more grateful to them for bringing back a childhood favorite.


It's really been amazing. Also very cool how much the community has grown in the past few years. The subreddit is super active which adds to the fun.


It's not just maintenance, Microsoft has released several expansion packs to the original Age of Empires II, plus an Xbox port to boot.


Even more amazing still is that it still has a large active player base and YouTube streamers make money playing it and casting videos of their play through. I'm routinely shocked by how easily I can join online multiplayer matches.

For a 24 year old game!

A bunch of guys in my company play it. Most of them were 3 or 4 years when the game was released. A couple of them weren't even born.

Real kudos to the team maintaining it. They constantly listen to player feedback and keep updating the various unit stats and bonuses to keep the game balanced.


Broodwar is still super popular and they don't do shit there either.


Yeah it appears my Diablo 2 has been pulled from my Blizzard account when the D2 Remastered was launched, at least I can't find it anymore. And now I either have to dig up the CD and key and hope those still work or pay $60 or something to play D2. My desire isn't really that big.


I see this link on the Blizzard support site that provides downloads to classic D2 and LoD but I haven't tested them out to see if they ask for a CD key or not.

https://us.battle.net/support/en/article/13867


This is a fantastic out of context change log.

For even more abstract ones I cannot recommend enough the Dwarf Fortress Bugs Twitter account[0]

The last of which is

> Flying creatures keep exploding into pieces and dying: Are they crashing into trees?

[0]sadly no Mastodon, https://twitter.com/DwarfFortBugs


Paradox Interactive games, especially the Crusader Kings series also have some pretty entertaining change logs. E.g., from Crusader Kings II, Patch 2.4 [0]:

- Handsome and lustful men now also populate the cabins in the wild for the pleasures of people who find them attractive.

- Characters who love their spouses very much are now less likely to join holy orders.

- Paranoid parents should no longer worry about potential plots against dead children.

- Fixed that bedtime stories can no longer be told to dead children

- You no longer punish yourself when a prisoner tries to flee from your prison by charming the guard.

- The Orthodox Patriarch is now concerned if Orthodox rulers have heathens in their court.

- Discovering two vassals of the same sex engaging in carnal activities during pagan feasts no longer let's the liege join in on the fun.

- Ambitious claimant adventurers now move out of the realm if they are targetting their liege

- No longer possible to banish your spouse or concubine

- Decreased monthly decadence gain.

[0]: https://forum.paradoxplaza.com/forum/threads/patch-2-4-chang...


Wow, the logic to model these kinds of social structures makes my head hurt. But you would have to do it.

And I wonder about dead children. I remember one game where there could be no dead children.


The logic is less complex than you'd think. It's generally something like "has flag paranoid, has flag child (patched to has flag alive child), then 1% yearly chance of fearing a plot." It feels complex, thinking of the actual logic killed a fair amount of the fun for me.

>And I wonder about dead children. I remember one game where there could be no dead children.

Probably Skyrim, though it's not rare. Crusader Kings somewhat encourages infanticide, as your brother's bastard could stop your character from inheriting a title.


> The logic is less complex than you'd think. It's generally something like "has flag paranoid, has flag child (patched to has flag alive child), then 1% yearly chance of fearing a plot." It feels complex, thinking of the actual logic killed a fair amount of the fun for me.

But doesn't this overlook the emergent complexity from many actors running these simple rules, plus interactions between them?

A lot of the shenanigans in Dwarf Fortress arise from relatively simple actors following their relatively simple plan, until those plans clash or interact with each other and crazy things start happening - such as prioritizing drinking booze over pulling a lever to lock out a monster.


>But doesn't this overlook the emergent complexity from many actors running these simple rules, plus interactions between them?

Somewhat, but it more feels like "wait for good flag, try to activate good flag events. Wait for enemy to have bad flag, try to activate bad flag events." Other's find it has lasting fun despite that, but I found I lost interest.


Starting sometime in the mid-'00s it became common practice to prevent players from gratuitously killing child NPCs. As I understand it, this was to comply with ratings boards. A good example of the change is Fallout. Fallout 2 had child NPCs with no invincibility, and in the German release they just made the children invisible and untargetable. This caused some issues. In Fallout 3 children can't be damaged by the player, but you can cause their offscreen deaths (with a nuclear bomb, even). Similarly, I think Crusader Kings can get away with it because there's no visual depiction.

As an aside, while I understand the motivation for this kind of thing, I remember finding a child's body after a firefight in Deus Ex and having a moment of moral anxiety as I wondered whether it had been one of my stray bullets or someone else's (or even unrelated to the fight).


If you don't want to see terrible things happening to children, I'm afraid CK is not the game for you. The game goes out of its way to model the worst aspects of medieval life.


And in CK you can actually sacrifice a child for some gains.


These are great, they make me want to play the game!

Although I'm not sure why a threesome during a pagan festival has become illegal, I'd have considered that a feature, not a bug.


There was a bug fix in Nethack a few years ago to make eating parchment spellbooks break vegetarian conduct. Which at one level makes perfect sense: of course parchment is an animal product. But the fact that it comes from the intersection of several kind of weird systems in the game made it such a great Nethack bug. First, that the game tracks vegetarianism at all. Then, the randomized unidentified item descriptions that are mostly flavor text but occasionally have gameplay properties. And finally, why are you eating spellbooks in the first place?


As opposed to eating rings, which can permanently give you the associated magical intrinsics (without having to wear the ring). This, in turn, means that which specific intrinsics you can get that way various from game to game (because you can temporarily polymorph into a metal-eating monster, but some rings are metal and others aren't).

https://nethackwiki.com/wiki/Eating_jewelry


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.


I credit that random UAE dude that was bankrolling TyranT and kept the game going. Without real money the game would not reach the critical level for Microsoft to pay attention again.


And some players have come a long way. One of my highlights was, when DauT won the Red Bull Wololo 3 tournament as someone who is playing the game 20+ years. And some fans are doing great stuff too, like this video that was released shortly after (DauT is back):

https://www.youtube.com/watch?v=_u-SisO8ers


I've never seen this video. Hilarious / awesome.


Not knowing it was about a game, that was one of the strangest headlines ever to read on here.


Conversely, I didn't even have to even click in to understand it hah. Been playing AoE for around 20 years.


I was confused as well, because I just had chatted with someone about whether the aoe2 monk conversion ability on certain buildings was hardcoded or not (they can't convert i.e. Town Centers, Monasteries) ... turns out it at least used to be, no idea about now.


Likewise, I thought it was something to do with blood pressure :)


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.


My metrication extension turned the title into "Aztec monks with 13.37 W no longer die when picking up or dropping a relic".


That's not a lot of power for a monk. What's the typical monk's TDP?


I had no clue what the headline was all about. I imagined some Aztec monks with a 1/55 HP motor trying to do something and just dying for no reason.


What a weird coincidence. I thought at first this would be about the Guild Wars game, but turns out they both have something to do with monks and 55:

https://wiki.guildwars.com/wiki/55_Monk


Ahh nostalgia. I thought so too. This was such a fun build. I miss GW1 a lot, GW2 missed the mark, and is no true successor.


Depends on what you're after. In terms of mechanics it's a completely different game, but lore wise a phenomenal amount is tied in to GW1. It's almost fan service: the game.


Man, I miss GW1. That skill/class system was amazing and still goes unmatched in its flexibility and depth today.


I want to create a spiritual successor to this but I'm only a dev, and not an artist. It's really killing me. I want to make a game like this before I die, but it's looking unlikely.


The servers are still running and it has had some QoL and even added some new Elite Skills and weapons.


Yeah, really the only word out of place in the headline was "Aztec". All the rest could have applied to GW.

Fun fact: In the early days (I think the first 6 months or so) of GW there were some item attribute modifiers that I think allowed for going as low as 5 hp, making the character basically invincible, as all damage was rounded down.


Wow, another 55 monk enjoyer. I used to farm hydras outside of the Crystal Desert with this.


There were dozens of us, dozens!


farming ectoplasm lfg


This game is still amazing. I never played it as a kid, but bought the Definitive Edition last year on steam. Already have > 600 hours on it.

Beat RTS ever in my opinion.

Check out big tournament to get an idea of the current scene: https://youtu.be/6krbA4jTUqE

Also I didn't play multiplayer until last month, most of that 600 hours was spent in the many many excellent single player campaigns


I don’t understand why they didn’t just use an integer. You can’t have half a HP in AoE and nothing has HP so high that it would overflow a 32 bit integer. Seems like it would avoid a lot of problems.


I had a really fascinating conversation with the lead architect at Frost Giant the other day. They're making the next StarCraft, basically.

They don't use floating points.

He didn't mention (IIRC) the possibility of rounding bugs; the reason he cited was something a bit more interesting: Floating points are not deterministic, which makes multiplayer challenging, as they simulate game state on each client's computers and only send commands across the wire.

One wonders if the "out of sync" errors that plagued Age of Empires in my youth were partially explained by FP determinism issues!


Floating point numbers are deterministic, they just depend on thread-global state which has different defaults on differen platforms and can be changed by third-party libraries (including injected ones) at any point.


Afaiu there are compounding factors like 1.5 here and 1.5 there should result in 2.25 and so on


I would consider banning implicit casting of floats to ints from the codebase: Nobody would have intentionally truncated in this HP conversion; they would’ve rounded.


It's an unfortunate consequence of compatibility with C.

I'm pretty sure you could get the compiler to warn you about this stuff though.


I was stopping myself from buying Definitive Edition but I guess I am buying it today...


Note that the April 2023 patch is live, and perhaps one of the biggest changes to the meta ever. I don't think anyone really knows how the new meta will shake out.

Build orders will absolutely change this patch. Though I guess that only matters if you were hoping to get to a competitive level.

Definitive Edition does have a very good tutorial mode for people interested in online combat, and "Extreme" AI is 100% legitimate (AoE2 Extreme does NOT cheat, you're fighting it fair-and-square, but Extreme does a proper build order so it just "feels" quick if you haven't studied build orders yet)


What’s new? Seems kind of crazy to introduce a meta changing past to an old game like this that has managed to stay popular.


Pre-patch, the meta was growing a bit stale and settling upon Crossbows play, or Knights play. (Or sometimes: Crossbows + Knights). An odd greedy defender might go Mangonel (aka: Anti XBow) + Monks (aka: anti-knights), but the game pretty much revolved around Archers (for civs like Britons) or Knights (for civs like Franks), and their closely related counter-units.

EDIT: case in point. Vikings, a civilization with numerous infantry buffs, was played as an Archer civ in practice because even with all their infantry buffs, XBow / Arbalest was considered stronger.

The biggest change this patch is a new tech for +1 pierce armor to the Longswords line, which can be researched in Castle Age. Furthermore, the entire line of infantry can upgrade faster, and cheaper. Finally, most infantry civs (Malians, Goths, etc. etc.) have gotten significant economy buffs to make them even stronger. Whether or not its enough for infantry to shine is to be seen.


I played aok a bunch and aom a lot more. As a dumb kid a decade ago.

It seems like a design failure that the game does not obey the intended rock paper scissors, no?


The rock-paper-scissors design of Age of Empires 2 (aka: Age of Kings) is more nuanced than what I described above.

So the meta pre-patch is that XBows are weak to Skirmishers. (10 E. Skirms can often win vs 20 XBows). Knights are weak to Monks (100 gold to convert a 60-food+75Gold unit is effectively a 120-food + 150-gold swing, so the Monk can die afterwards and you still got an effective conversion. Furthermore, Knights are the same speed as all other knights and have a ton of armor+HP, so the converted knight can likely escape).

If the monk survives for a 2nd or 3rd conversion, even worse. The monk already paid for itself after just one conversion.

The "intended" rock-paper-scissors was that Infantry (ie: Longswords) were countered by XBows. But...

You see, its more important to "force" your opponent into making otherwise worthless units. Skirmishers may counter XBows, but they're nearly useless against all other units in the game. So by "forcing" your opponent into a counter-unit, you've more effectively controlled the game.

So Longswords are countered by XBows. But guess what? XBows were the meta-play to begin with, so you just "forced" the opponent into making good units, what he should have been doing from the start!

-----------

That being said, XBows only counter Infantry when micro comes into play: effectively knowing the XBow move-and-attack cooldowns. Its not a thing beginners do. If you just patrol-move into a pile of infantry, the Longswords kinda do okay.

So its only a counter much like how monks "counter" knights, I assume perfectly play on the part of the XBows (who have nearly the same movement speed, so they can perform a "retreating" kiting maneuver against enemy Longswords). If you are unable to micro like this however, its not really a true counter.


There has been drastic meta changes before too but over the years the units that have been made has remained the same. The earlier meta mostly used to be around archers or cavalry , infantry was simply not viable except with certain civilization(goths) and that too only once you had the whole economy set up to spam their cheap infantry continuosly.

Theve been trying to push infantry with small buffs , and this is patch has seen some more buffs to infantry and infantry civs.


In addition to the infantry gradual bufs, full-walling in Feudal using a mix of palisades and houses became ubiquitous. Pallisades, buildings-as-walls etc were gradually nerfed. Still popular, but now there's a higher associated cost. I wonder if they're done with those nerfs, or if there's another coming. (Since this technique is still very strong... Or maybe it's that scout rushes are strong!)


How was walling with housing nerfed? I remember doing that a lot.


In-progress buildings are easier to destroy IIRC. Palisades cost more and are slower to build.


More buffs to infantry. Players got better and infantry was barely used.


Several infantry civs like inka, sicilians, slavs, vikings, celts and dravidians got many huge buffs. Main change to like 50% of the civs is called gambesons, which adds 1 pierce armor to the militia line, basically reducing the damage they get from skirmisher by 50%.


> old game

They have been releasing expansion adding new civilizations every ~6 months or so together with a bunch of balance changes.

> stay popular

It didn't. Not too such an extent as it is now. For years it had only community support and a fairly small player base. Microsoft resurrected it not so long ago.


To be fair, it may be the "biggest change ever", but it doesn't change too much. It buff some units that were bad, nerf some units and techs that were too good. But the general meta remains the same. The devs are really good at introducing changes.


What's up with all of these 18 feudal builds?? And to Hell with the Portuguese fast castle, organ gun OP on arena.


It was always possible to do 18 pop feudal, but today's focus on luring deer is perhaps the biggest change to the meta?

There wasn't any real balance patch. Just people who have gotten much better at luring deer to improve early build orders. Like 3 years ago, you'd just assume 2x Boar + 8x sheep as your early food source. But today, people are adding 2x or even 3x deer to that.

As deer + boars collect food at the fastest rate, you can more consistently go up at lower villager counts. There's also much more precision: killing boars / deer / sheep under the town center more consistently today than before.

--------

TL;DR: Skill improved, people can do 18 pop today even though its always been possible. So people are just getting faster and more optimized.


Hera made a video about this recently and attributes it more to all the QoL updates like shift queueing. Sheep actually spawn next to your TC now. Before DE you had to find your first 4 as well.


Oh, one more thing. April patch changed Portuguese Organ Guns.

The goal of the April patch was to make organ guns more of an anti-unit shotgun, and less of a wall destroyer. So you'll see fewer organ guns on Arena (one of the few maps that start off fully stone walled).


ok, but who but TheViper or DauT has this issue? ಠ_ಠ (As a Japanese main, nerf Aztecs.)


The T-West video talks about a match between Mr. Yo and Valas ;-)


Wow this comment had me reminiscing of watching AocZone tournaments replays of them in my youth

Sigh


Always a little surreal to see the Viper still around. He used to be a legend in the forums 20 years back.


What’s the best way to play AoE on a MacBook? I can’t afford a dedicated game rig so any pointers would be of great help.


Probably not the answer you were expecting, but you may want to consider getting an Xbox Series S (that is often on sale at $250 or even $200), plugin your mouse and keyboard and you'll have a great experience. Yes that is a "dedicated game rig" but much more affordable than a usable gaming PC. Of course, only if you also play other games on that xbox and can live with no disc. This is what I would do in your situation.


This is what I found for the M1 https://old.reddit.com/r/macgaming/comments/ms8081/age_of_em...

Seems to work fine with Wine and Crossover.


I use a small machine at Paperspace (P4000 with 100GB) and play it remotely with Parsec. The experience is very good and costs me about 10-15$ a month.


How is it that a modern laptop manages to be worse at gaming than the average PC 20 years ago? Surely you one can at least run it in an emulator of some kind?


You can just run with Wine, it seems to work mostly fine.

Also you could probably just install ARM Windows in a VM, not sure how good is the Windows translation thing compared to Rosetta though you might end up with a worse experience than just by using Wine.


When read with a history bias, that title makes for a long and deep source of confusion.


I mean, Age of Empires 2 is a very historical game. Everyone knows that the Halberdiers of the Inca are pretty good especially when paired up with Inca's famous archers: their Crossbows and Arbalesters. /s

These historical games have two ways of doing things. Staying historically correct (which is unbalanced... such as Hearts of Iron where Soviets + Germany will likely crush Poland in the first move every game), or you balance the game so that everyone has a fair chance like AoE2. But balancing the game naturally introduces significant ahistorical aspects.

--------

AOE2's historical campaigns are pretty good though, and somewhat representative of famous battles throughout history.


Suppose someone with 1 hitpoint actually transformed into a unit with less hitpoints - should they then die? Seems to me the fix should be to represent hitpoints as integer values, and round up during conversion.


Well, once it's out it's out. O can just imagine fixing this would break some clever mod/campaign featuring deadly relics that monks are forbidden to touch.


Is Age of Empires playable in MacOS? I’d like to remember my childhood years but going back to windows after 15 years of Ubuntu and MacOS gives me headaches.


You're a decade too late, there was a ppc port for MacOS back then ...


What kind of monster would write this as `new_maxhp * (old_hp / old_maxhp)` instead of `(old_hp / old_maxhp) * new_maxhp`.


Immediately noticed how the font on the MS's site is very readable. "noto-sans", evidently


Guess that’s what those Epsilon checks are for.


Rather than linking to this Reddit thread, wouldn't it make more sense to either link to:

* The linked-to patch notes? https://www.ageofempires.com/news/age-of-empires-ii-definiti...

* The particular comment explaining this bug fix? https://old.reddit.com/r/Games/comments/12jbb9d/age_of_empir...


I tried to and thought I had linked to the comment. I did wonder if the link went only to the thread, but the Reddit mobile web interface is so broken that I couldn't tell where the link was going to, though I figured the Aztec monk bug would stay at the top either way so people would figure it out.

(the official patch notes don't explain the bug)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: