Saturday December 4, 2021; 8:31 AM EST
- (9 min read)#
- I got to take the wheel for a short ride yesterday, which was spectacular, like going to the mailbox, but even better. I went to the office to unstick my keyboard, and pick up a few books, including JavaScript Patterns, an O'Reilly book with a European partridge on the cover, by Stoyan Stefanov. This book defines pretty much the extent of my knowledge of JavaScript that I don't get from random googling into Mozilla's docs. It clearly lays out pretty much every single way you can do stuff in JavaScript, and spells out some of the advantages and drawbacks of each approach. #
- I found this book to be invaluable the last time I spent a lot of time with JavaScript, which was back in 2010. At the time I was putting together a virtual tabletop application that would run in the web browser.#
- A virtual tabletop is a shared space for playing tabletop roleplaying games. These games are all different, by nature, but typically a tabletop roleplaying game is one where several people sit around a table, with a map, on which there are pawns, or tokens, representing the characters they are playing, or fighting, or negotiating with, or looting. Lots of dice rolling, hundreds of pages of rules determining outcomes, lots and lots of ways to customize your characters. Typically, one person will prepare an adventure, and run the game for the other players. This may span dozens of hours over several sessions of play. To make this easier, you can buy printed adventures, maps, tokens, prepainted miniatures, miniatures you can put together and paint yourself, etc.#
- The virtual tabletop I built was intended to simulate all that stuff, in your browser, ten years ago, in Safari, on your desktop, on an iPhone 4, on an original old-school iPad. It had print-resolution tokens and maps that you could drag around and zoom into like Google maps, until you could literally see the individual fruit in the cart in the village square. Group and private tabbed in-character chat, like Slack, except with a non-stupid UI. Dice rolling, dice macros, combat initiative tracking. The ability to sign in simultaneously on multiple devices so you could run initiative on your phone, use your iPad for chat, and your desktop for the map. I had the opportunity to get up on stage and demonstrate this to hundreds of fans at a convention, and promise early access to all attendees. Testers were thrilled. For strategic reasons, just before final public release, that project got canceled for something else, which later got canceled for something else, which happens.#
- Anyway, to make something like that, you really don't want to be subclassing everything, it's a nightmare. Let's say you have a Token, which has Token.x and Token.y. Then you have a monster, well, OK it's a Token too. Then you have a spell, which takes up a bunch of squares on the map, in a weird cross-type shape. Is it a Token? Sure, it's a thing you can place on the map at a particular location, and the shapes of Tokens can be determined by their size or cross-section or something.#
- Now you want to place a thing for the person running the game to click on. The other players can't see this control, because it's for a prepared adventure, and displays some hidden text about the room and its contents. Is that a Token? Well, maybe it's a Button, which also has a Button.x and Button.y. Tokens and Buttons are totally different. Tokens can be moved around. The x,y of each Token is going to be persisted separately, for each virtual tabletop. The Button, on the other hand, has an x,y but it only needs to be persisted at the Adventure level, which means, we only need to store it in one place, because every virtual tabletop will see it at the same place.#
- So it's very unlikely that you will come up with a common superclass for Token and Button, in this type of problem space. Particularly because to add new features to the application, you need to consider the front-end user interface, client-side application logic, client-server synchronization protocols and message formats, client-to-server-to-clients synchronization, server-side request-response handling, server-side application logic, data consistency, caching, and back-end data stores. The approach I chose was an entity-based system, taken from MMO video game design. Functionality was added by compositing attributes and abilities. #
- In this approach to the example above, there's not really such a thing as a Token. You'd create a Thing, and you'd give it a Position, which means it would have an x,y. For a player character, you'd also give that Thing a reference to a Player. A button, on the other hand, would be a Thing with a Position, which also has a reference to a Narration. #
- This object, each Thing, is dynamically constructed on the fly, based on definitions from the server. We never create a superclass. You can have ten different Things, each with different sets of attributes and capabilities, and each absolutely interchangeable from the system's perspective. As long as a Thing has a Position, and is Selectable, it can be either a token, or a button. #
- Literally everything is a Thing. There are no subclasses. Each Thing, of course, has its own unique collection of capabilities (functions). The application control logic, of course, needs to have a meta-type understanding of capabilities, but it's easier this way rather than a bunch of if-then or case statements. We did use a lot of state machines, I guess, but state machines should be considered fundamental, like arrays, or hash maps.#
- This is the sort of thing that's easy, or at least doable, in lots of languages. LISP is the classic, you can composite together anything you want, though I've never done enough LISP programming to know if that's actually how they solve their problems in the vernacular over there. I could make a pretty decent composition system in FORTH, too. No, I have no idea why I keep reaching toward FORTH and LISP as my go-to comparisons for JavaScript, why is that? Curious. (It would be a lot of fun to think about building the whole thing in Rust, actually.) #
- In the real world you're going to use JavaScript, client- and server-side. It's suited just fine to this sort of problem, actually. But JavaScript gives you a lot of choices about how to do things, and you're going to get pretty deep into the weeds of every single possible approach to figure out the best way to stitch together these little composite entities dynamically at runtime, and still maintain your sanity, debugging and QA. #
- Anyway, here are some gems, retyped or paraphrased from the book. I guess these are some of the things that, with Drumkit, I am working on ignoring. I am eliding the hell out of these quotes, but I don't think I've changed their meaning. Stupid typos are naturally mine—#
- #
- Any variable you don't declare becomes a property of the global object. [...] Regardless if created inside functions, […] properties can be deleted with the delete operator, whereas variables cannot.#
- #
- If you need to access the global object without hard-coding the identifier window, you can do the following from any level of nested function scope: var global = ( function () { return this; } } () ); This is no longer the case in strict mode. [...] You can wrap your library code in an immediate function, and then from the global scope, pass a reference to this as a parameter to your immediate function.#
- #
- [Paraphrased] You can't iterate over the properties of an object, because some of those properties might come from some other object. So you need to check that object's hasOwnProperty method to see if it's a real property and not, say, some random function named clone(). But, well, that hasOwnProperty method could have been redefined. So what you end up looks like this: for (var i in man) { if (Object.prototype.hasOwnProperty.call(man, i)) { … } }#
- #
- [Paraphrased] You can augment the prototype property for built-in constructors such as Object(), Array(), or Function(). That's right, you can add new functionality, entire functions of functionality, to functions defined with Function. This leads to super-awesome code. (OK, the book is good, and so actually says, "it can seriously hurt maintainability", and then details why.)#
- #
- JavaScript implicitly typecasts variables when you compare them. That's why comparisons such as false == 0 or "" == 0 return true.#
- #
- [Oh right, I forgot about eval(). Remembering that eval() exists is like losing the Game.]#
- #
- This code logs undefined: (function () { var local = 1; Function("console.log(typeof local);")(); }());#
- #
- The Object() constructor accepts a parameter, and […] may delegate the object creation to another built-in constructor and return a different object than you expect. [ie, you can do new Object(1) as well as new Object("1"), and also do not do this thing.]#
- #
- Self-invoking Constructor. To have prototype properties available to the instance objects, consider the following approach: In the constructor you check whether this is an instance of your constructor, and if not, the constructor invokes itself again, this time properly with new.#
- #
- ECMAScript 5 defines a new method Array.isArray(), which returns true if the argument is an array. […] If this new method is not available in your environment, you can do something like this: if (typeof Array.isArray === "undefined") { Array.isArray = function (arg) { return Object.prototype.toString.call(arg) === "[object Array]"; }; }#
- #
- To illustrate the difference between a primitive number and a number object: typeof 100 is "number", while typeof new Number(100) is "object". […] When used without new, wrapper constructors convert the argument passed to them to a primitive value: typeof Number("1") is "number", as is typeof Number(new Number()), and typeof String(1) is "string"#
- #
- Yet another syntax that accomplishes the same results is…#
- #
- In the next example, the value returned by the immediate function is a function, which will be assigned to the variable getResult and will simply return the value of res, a value that was precomputed and stored in the immediate function's closure…#
- #
- In other words, both of these work: ({ … }).init(); ({ … }.init());#
- #
- I've been skipping stuff, and we haven't even gotten to function memoization, partial application and currying; general purpose namespace functions, like var MYAPP = MYAPP || {}; implementing private members via closures; avoiding recreating private members each time the constructor is invoked; globals; sandboxes; modules; public and private static members; so much discussion of __proto__; prototype chains; simulating classes; prototypal inheritance; mix-ins; borrowing methods; binding prototypes. And a bunch of other stuff, it's a good book.#
- I know some of these examples aren't fair, we're probably up to ECMAScript 12 or something by now, time marches on, and JavaScript rules everything around me. And I know Drumkit is still JavaScript underneath, really. You can probably use any old kind of JavaScript insanity you like, I am pretty sure about that. #
- But for me, in addition to the pre processor, Drumkit is a voluntarily chosen subset of JavaScript. I am taking that to an extreme, I am omitting semicolons like no tomorrow. I am trying to find my Drumkit vernacular right now. All sorts of options for every decision, which I am busily paring down to the set I prefer.#
- I have absolutely used many of the bananas features available to me in JavaScript before. And I may do some low-level JavaScript shenanigans at some point, somewhere. But that will be for a specific, narrow, purpose: to make the rest of my Drumkit as plain and obvious as I can make it. #
- #
- See also this previous post on JavaScript.#