I've been leaving far too many things variable between playsets. In fact, the number of truly distinct game modes requiring different paradigms is SMALL. All top-down 2d games can be thought of as special cases of a general top-down 2d gameplay case, etc. No one but me is likely to add game modes, and even if they do they'll probably be adding them within a general case I wrote.
Input events are of a predefined shape, always reporting on specific key and mouse actions, plus special menu commands when they happen. (Client-side key customization should be permitted here, so don't refer to literal keycodes).
Users have a predefined-shape block of customizable avatar data. This is distinct from the game state. It can change only while in the lobby instance, and within the lobby instance it is not used by deterministic game logic. Changes to it may be propagated asynchronously if that makes the code better.
Playsets are not separately encapsulated disjoint entities. There are still named game types that are used when deciding which game logic to fire, but game logic does not have to belong to an object keyed to a specific game type.
All game state serialization/deserialization/hashing goes through one general serializer/deserializer/hasher. This will probably need to eventually support at least DAGs and possibly fully cyclic references.
There are game instances. An instance can have a cap on the number of players that can join it. Within an instance, players share a prediction/rollback game state. The game type of an instance is not fixed at creation time; there can be code in a game type that transitions to another game type in a deterministic way. (The player cap might need to be fixed at creation time.)
The only time a player moves between instances is traveling in or out of the distinguished main lobby instance, which has no player cap and no complicated game logic. Instance out/in transitions are distinguishable from server connect/disconnect, but end up going down the same code paths mostly.
A game state contains an ordered list of player sprites or otherwise has a way of managing its own player order. Inputs come into a game state as an unordered username->value object, which is not meant to be iterated. Joins/parts come into a game state via ordered discrete event handlers (only).
The set of instances is defined in server-side configuration code.
The server doesn't have a distinct "config file"; configuration values are in a Node module.
An instance dies if no one has been in it for a while or at server stop. Instances start dead on server start. When someone tries to join a dead instance, the server starts the instance. Instances have an on-death that returns a persistable object and an on-start that takes that object (or null) and an RNG. On server stop, these objects along with the user table get written out to a file (and the previous file if any gets renamed away). For now, don't worry about bulletproofing this file process.
playset-specific UI widgetry is not a thing. There are specific UI modes that know what to look for in specific kinds of game state, most importantly one for a 2d grid of tiles and sprites and one for a 3d grid of terrain blocks and sprite characters. Instead of forcing everyone to scaled multiples of the same screen size, the UI fills whatever amount of screen it has available, and zoom is selectable in 2D UI. Editor functionality can go into these UIs, to be unlocked by whatever game states want to unlock it.
initially, worry about just non-editable top-down modes (and the avatar config ui in the lobby), but make sure there are clear branch points in the UI for others
I've been leaving far too many things variable between playsets. In fact, the number of truly distinct game modes requiring different paradigms is SMALL. All top-down 2d games can be thought of as special cases of a general top-down 2d gameplay case, etc. No one but me is likely to add game modes, and even if they do they'll probably be adding them within a general case I wrote.