NIMBY Rails devblog 2026-10
Pax commuters
The theme for v1.20 is a rework and improvement of pax mechanics, and the first item was implementing pax commuters. In v1.20, when a pax spawns, there’s a chance it does so as a commuter pax. These pax store two extra bits of information: a flag that indicates they are a commuter on the arrival trip, and the time they plan to start their return trip. During their trip they are otherwise normal pax with no special rules or preferences. But on arriving to their destination, rather than being deleted like a normal pax, they are instead stored in a separate special hall of the destination station, called Commuters, and given a hall waiting time that will put them back in the station at their preferred return trip start time. This new hall stores pax in a compressed way, discarding most information, to be able to store large amounts of them. At the return time the commuter hall pax are spawned in the station platforms as full featured pax, their destination is set to their origin station, and they are flagged as commuter pax on a return trip. Just like the arrival trip, the return trip is a normal pax trip without any special behavior, just the flag for UI purposes. Non-commuter pax can still spawn and they are flagged as one-way pax.
So what decides who is a commuter pax and who is one-way? On spawn, pax already have an origin and destination station pair, and additionally in v1.20 a random “away time” is calculated using a power distribution controlled by a new curve assigned to the demand curve the pax is being spawned from:
This is a new curve in the Demand system, and demand mods can provide it in the curves TSV. The X axis is the week-time, and the Y axis is the away time. The min and max range for each 1h node is a hard cap, and the mode node concentrates most of the distribution of values. I am leaving this kind of vague because I’m not sure this is the 100% final design, but it works fine for now.
Now you have a pax who plans to be away for a certain amount of time, and you know their origin and destination, so the return trip goes from destination to origin. Therefore it is possible to issue a pathfind, timed in the future, to lookup what is the best path from destination to origin, departing from the destination at a certain time that guarantees the pax will back at the origin at very close to the randomized away time. Additionally, the return trip should not take too long compared to the arrival trip (the slack right now is 1.5x). If all of these conditions hold up, then the pax is flagged as commuter-arrival, and the expected return time is stored in the pax. If they fail, the pax is spawned anyway, but flagged as one-way.
If your network is urban oriented, it is well connected and you made sure to provide plenty of service for the morning and afternoon rushes, then your effective amount of pax will roughly double in v1.20 due to commuter return trips. You might want to consider reducing global pax spawn rate and/or tweaking demand curves.
Price-costed pax pathfinding
Tycoon fans, there’s a rare Tycoon Concession (TM) being made in v1.20: pax pathfinding will now also consider the price of train legs, not just the arrival time. I say this with a bit of irony because of course it’s obvious to everybody that every train game ever made needs to be about setting prices to things and seeing the numbers go up. All this stuff about wanting to differentiate like “what if a train sim supported 1:1 time-space scale and 1s precision timetabled pathfinding over thousands of stations, millions of departure events and millions of pax at over 100x real time speed with a just a few seconds of concurrent pre-processing time” are just unnecessary fluff compared to what is truly important in a simulation videogame: setting the price of burgers in the food stall.
There’s no food stall in this game so it will be forever an inferior simulation, but at least in v1.20 pax can now consider if the price of boarding a specific train is worth it compared to the price of other trains. The central idea is that there is a way to transform money into pseudo-seconds, and that pathfinding is now costed in pseudo-seconds. In v1.19, when the pathfinding needs to give a cost to the act of boarding a train and ride it for some time (called a “leg” here), it calculates this:
cost(leg) = leg_ride_time
In v1.20, it does this instead:
cost(leg) = leg_ride_time + leg_price * price_sensitivity
Immediately it can be seen that, if your lines are priced as 0, the pathfind should be the same in v1.19 and v1.20. But if the line has a price, the leg now has an additional cost over just the ride time. This simple formula makes it possible for an expensive but fast train to become undesirable if the price to ride it is so high that it eats up all the time advantage compared to a cheaper but slower train.
To properly support this idea, line pricing had to be redesigned. Fixed line pricing has been removed from the game, and only price per km remains. Additionally pax now pay fares on boarding, so at the start of the leg. This makes the simulation match the pathfinding behavior (and real life to be honest).
So what is “price sensitivity”? It is a global constant common for all pax. As you can see it from the formula the math side is a very simple concept, but it’s a bit hard to communicate in the UI. There is a tunable slider to change it, and this is the wording choice for now:
The text changes as you move the slider, giving you a live example of the effects of the setting. It was the less bad idea to try to explain it intuitively, I will see during beta if there’s a better way.
This new cost formula required changes to the pathfinder heuristic that make it less reliable for producing optimal paths (unless you set the pathfinding bias to 0%). In fact the concept of per-line pricing by itself makes the heuristic worse to help the pathfinder find good paths quickly. A given trip with a large variance of costs over the day or week was already a challenge to represent properly in the heuristic table, but at least in v1.19 the variance was more limited due to the simulation itself (maybe the worst trip was 3x as slow as the fastest one for example). But with this new system the price component could massively dominate when very cheap and very expensive trips alternate between them as “best trip” for any station pair as the day advances, making the heuristic a very poor proxy for both of them, forcing the pathfinder do more work (so it’s slower) or have to ignore possible good options to find the path faster. For this reason I am capping the line max price per km to $2, and including warnings when the price sensitivity is set to medium or high values. You can still set it to the max, and go set your lines to prices like 0.0 and 2.0 with no range in between, but at least the leg_ride_time component is still there to try to dampen it a bit without being massively small compared to the price component.
Transfer penalty
Another way to further dampen the potential destabilizing effect of very large price variance is to add a fixed cost when boarding a train on a transfer. This is also a common player suggestion. So in v1.20 the pathfinder cost formula is actually more similar to this:
cost(leg) = leg_ride_time + leg_price * price_sensitivity + transfer_penalty
The transfer penalty is the simplest possible value: a single constant number that is applied to the pathfinder cost of every train boarding, independent of price, time, line, station, etc. In other words there’s no effort to try to model any fancy simulation aspects like inter-platform vs intra-platform transfers, security or ticket control delays, etc. This is what makes it applicable to the formula. If it was derived from some other proportional or dependent axis it would then just become another variance source and it would do more harm than good.
To make the predicted path match the simulator behavior, the option to make transfer pax move to the hall before they are allowed back on the platforms is now the rule. In v1.20 transfer pax always move first into the hall, stay there for some time, and then go back to the platforms. This forced hall stay time is also a slider in the UI:
Satisfaction and NIMBYism are out
The old, half broken, very limited “Satisfaction” aka “sax” pax system has been removed in 1.20. It was a remnant of a much older system that actually influenced station pax spawn rates, but it was very flawed from the start, and other pax mechanics had more priority.
Sax was being kept around because the idea was to evolve it into the NIMBYism feature. Not as a path/price satisfaction mechanic, but more about how its original incarnation influenced pax spawn rate over a length of time, potentially over months. The old code for that feature was being kept around, just disabled, waiting for NIMBYism. But now I’ve decided to not implement NIMBYism, so it was the last reason to keep sax alive.
Why removing NIMBYism from the roadmap? Because now I see it as more of a tycoon-like feature than what the game is nowadays. Six years ago the specific of what the game was, and what it was meant to become, was looking closer to a traditional tycoon than the more sandbox hard-simulation it has become. I very much prefer the current design than the tycoon-like ideas of old, it makes the game more unique and interesting. Less “game” for sure, but there’s niches for all kinds of designs, and there’s excellent transportation tycoon-likes out there already.
At the same time, NIMBYism would become the poster child of something that happens again and again in this game: I spend a lot of time and effort to design and implement a new game system, and the immediate reaction of some in the player community is to ask for a button to disable it. Notice how I am constantly describing new features like “if you set it to 0 your save doesn’t change”. When this is not offered it becomes a little drama. Everything must change but nothing can change, that’s how it feels for me.
So NIMBYism is out and I believe the overall evolution of the game and enormous amount of non-roadmap features it has gained over the years more than make up for it. You can now make the meme “NIMBY Rails does not have NIMBYism”.
Pax categories
I described the new parameters for pathfinding, price sensitivity and transfer penalty, as “global”. This means that they would be the same for every spawned pax. In an ideal world (or a world where players accept their formerly 1M+ pax sim is now a barely 10k+ pax sim) every individual pax can be as unique as a snowflake, with distinct preferences for every possible knob, while keeping the same timetabled path complexity. This could be accomplished by the mythical goal of caching the initial pathfound path per-pax. But I posit that even in this hypothetical design, it’s still a bad idea, unless players always provide a perfectly running network with no delays. One missed train and you are back at recomputing paths, and since the pax have unique preferences, it cannot be a cached path.
In v1.19 and earlier there were no knobs to influence pathfinding, so there was not much of a need to make pax unique, but now in v1.20 the existence of price sensitivity makes it a lot more justified. Price sensitivity will help in the general case of pax boarding fast long distance trains that happen to make a few urban stops for better service coverage, for the purpose of making very fast but short local trips. But it won’t help the case of these fast intercity trains erasing all the usage of the slower regional that makes more stops and it’s 50% slower overall. There will be now a combination of line price, speed and price sensitivity that puts pax in one or the other, but since the parameter is the same for all pax, one of them will remain near empty.
Between unique snowflake pax, and all pax sharing the same brain, there’s a middle ground: pax categories. The knobs are still global parameters, but rather than having one global set of knobs, there’s now N global sets of knobs, each set a pax category. When pax are spawned they are assigned one of these categories, and from that point all their pathfinding decisions are made using the knobs values corresponding to their category.
If N is small (current choice is a fixed N = 8), the number one feature sustaining the sim performance, the per-station departure cache, can be maintained. It now becomes N caches, one per each pax category. To be clear this is still multiplying per N number of required pathfinds overall, but there’s also mitigation for this. For example, the choice of category can a very biased distribution, so there’s one very dominant category, with most pax belonging to it. This means its cache will see a high hit rate, improving perf. The other categories can spawn less pax and in turn require less pathfinding, since there’s pax in the system. Additionally allowing to fully disable a category can help players control the performance impact. The more categories you disable the less extra pathfinding will be required. The spawn distribution editor supports both features:
Each category can have between 0 to 100 ballots, this is a hidden number. Dragging the bars up and down moves the per-category ballots between these two extremes. To arrive to the final % spawn probability (this is the numbers displayed in the plot), the per-category ballots are divided by the sum of all the ballots. It’s a very basic discrete distribution.
This system won’t enable snowflake pax, but instead you can have a few active pax categories that display different behavior. Your parallel train routes can now get simultaneous usage and you can control how much impact you want to tolerate in your sim speed. The default state, both for new and old saves, is to have a single enabled category. But it already has some non-0 default values for both price sensitivity and transfer delay.
There’s still a lot of work left to polish v1.20 and do more testing, so don’t expect beta soon, but it’s not too far away either.