API for scheduling actions that do not run every tick
I agree with the Game.seed suggestion. It's a nice way to balance the tick times and avoid massive operations in 1 tick followed by nothing the next.
As a general philosophical principle, my preference is to slip any changes in at the lowest possible level so that the observable consequences are minimized.
Instead of introducing a new API function (which doesn't solve the problem unless people rewrite their code), or introducing a seed or player id value that can be added in (still requires people to rewrite their code), we should at least try to think of a solution that works even if people don't rewrite their code.
One obvious choice is to simply return a different value for game.ticks for each player. The server could simply have a unique offset for each player that it adds to the real tick number. That would immediately fix the problem. Of course it's not really unobservable; you'd get confused when you tried to match your logs with the replay, and you probably want to be able to coordinate attacks using the tick number as a clock.
So here's my real suggestion: change how mod works. Simply change the mod operator so that it adds the player's id number to the first operand before doing the mod operation. This doesn't change the domain or range of the operation, or change its behavior or usefulness, but it does solve the problem. On the down side, modifying v8 in this way could be annoying. You're forking a complicated component, which will have a maintenance cost down the road. Also, you'd have to dig into the JIT compiler so that it generates the correct machine code for the modified operation, which is not likely to be trivial. But from the philosophical point of view it would be ideal.
@db48x : how does changing the mod operator not change it usefulness? if you change it as suggest it is simply broken and results will be unexpected.
> how does changing the mod operator not change it usefulness? if you change it as suggest it is simply broken and results will be unexpected.
When you write Game.time % 10 == 0 (or any other constants), what's important is that this is true every 10th tick. If you change the behavior of the mod operator so that it adds a constant to the first operand (so that a%b becomes (a+c)%b), all you do is change which ticks this expression is true for. It doesn't change how _often_ it's true. This is better than changing the value of Game.time for each player (so that when your code sees Game.ticks == 42, mine would see Game.ticks = 53, even though it's really the same tick), because it doesn't hamper coordination between players, or between the player and the replay data.
@kotaru may have a valid objection that this would have unintended consequences, though I don't quite see what it would break for RoomVisuals. I guess if you wanted to use RoomVisuals to make a grid overlaying the room map? If you wanted a line across the map on every 10th row and column, to make a grid, then this could indeed be confusing. The x=0 row would not have a line when you did x%10==0, but it would still be doable. You would "simply" need to try the other constants 0 through 9 in the conditional to find out which one works. (Or figure out what your per-user constant is first, so that you could do the arithmetic to compensate). Or use a loop that increments by 10, like a normal person. I really think that the other downsides I mentioned are worse.
Because a colour visualizer will often use a transform e.g. `dist % 10` for comparing adjacent cells. Especially when you don't care about absolute value, and only relative difference, controlling colours like that is common (you see it very often in depth maps).
The major problem is that a user will unconditionally expect `a % b` to be the mathematical output, and that no behind-the-scenes transforms are applied to either, in any circumstance. This is a problem with any override.
People will use % for more cases than just Game.time, and the prior was just an example.
Oh yea, if you use mod to get repeating color bands in a distance field, it would come out off-center with that modification. That would be pretty surprising, and therefore bad.
Of course, when you're mucking around at that level you really can have two mod operators, one which only ever works on Game.time, and the other on anything else. You're sort of starting to add epicycles at that point, however. Or Game.time could return a value that is of a new type which subclasses Number, so that it can do all the usual numeric stuff, but with the trick mod operation. But then it's a boxed value, and that's not free either.
A fine problem!
Incidentally, I think the API you suggested is quite well-designed. I was going to suggest passing in a function like this:
Game.createRecurringTrigger(10, repeat=true, name="labTimer", runLabReactions);
But this has a big down-side, because the specification for when that function gets run becomes problematic (do they run before or after the rest of your code, what order do the triggers run in, etc). With the conditional you gave you can run the triggered code in whatever part of your code that you like, which is nice. On the other hand, I'd change the name "getTrigger" to something like "checkTrigger", because it really is a predicate. It doesn't return some object, it returns true or false.
It's not just about colors... It's about all stuff expecting a working modulus operator. There is a ingame encryption library for example to exchange data with your alliance over public segments. How long do you think somebody would need to debug his code until he finds out that someone messed with the operator. There is no place to document that sufficiently enough to not be a potential major pain in the ass.
Breaking the expected behaviors of common operators is a very dangerous move. I don't ever think it's a good plan.
As W4rl0ck points out, this would completely break the encryption module developed for public segments. I'm sure there are a variety of other things that we aren't considering which would also break. It would also require essentially forking node as far as I can tell.