Game dev. The art of writing games. It's a little bit of a childhood dream of mine to write a game. A game, which is enjoyed by many people. A game, which reflects the world of my dreams. I can do whatever I want, and show other people what I think about about, what my 8th grader syndrome made me see and how everyone can be a distinctive hero there. My own little sandbox. I guess, it's this kind of motivation also driving other people into game dev.

However, game dev... there's so much going on, so many things to do and consider. Big dreams are one thing, but where to start is a completely different world. With this article, I want to introduce you to game development. I want to give you a little tour of where to start, how to explore possibilities and revive my journey a little bit at the same time. I will try to construct different games from the ground up while writing this article, so you can experience game-dev live. For now, I am no match for the likes of Casey Muratori, who streams his sessions, which is why I would like to stick to writing for now :) Also, don't expect regular updates, as I have quite a lot on my plate, so I will write whenever I find time. I hope the situation improves in the future, though.

This article was first released for Rust-Lang, which is a low-level alternative for max-perf game dev, however I ported it to meet your web needs!

Content

Requirements

Before you read on, if you plan on playing around with code yourself, please make yourself comfortable with HTML, CSS, JS, SASS and TypeScript. This article series requires you to have basic knowledge of the languages and the necessary tooling set up. I will not explain the finer details to you, if not necessary for game development itself or understanding a complex block of code.

What is a Game?

Ok, with that out of the way, let's start. Game dev. First of all, what we want to do is start simple. What exactly is a game? Taking a look around, there are many types of games. Card games, sports games, table-top games, pen-and-paper games, video games, and many more. They all have in common, that they serve the purpose of entertainment and that they have a set of rules, which define the structure in which the game should be conducted. Usually, it does not matter how complicated the rules are, as long as it's fun, anything can be a game! This realization gives us the freedom to make our dreams come true, or at least find something to do over boredom.

In order to start out developing a game, we need to first settle on a few things:

  • What type of game do we want to create?
  • How can a player interact?
  • What should be the game rules?
  • What is the goal?
  • Why is it fun?
  • What language do we want to use for programming?
  • Which platforms do we want to support?
  • Which tools do we need?

What is our Game?

Since we want to start at the very beginning, let's start with one of the simplest games we can come up with: a child's game called "Guess the number". It should be a text-only game, because text is very simple to handle. A player should just use the keyboard as input. It's standardized and easy to use from most programming languages. The rules for our first game should be, that a player has to guess a number thought up by the computer. The computer has to tell the player if the number is greater or smaller than the thought-up number. The goal is to guess the correct number. It's fun... well, it's mostly for kids who want to brag about their new-learned math knowledge. For adults, it's fun to show off and play their first self-made game ;)

game.jpg

As for the technical aspects, we want to go with a modern setup. As such, I choose the web as a platform, which is very good for distributing games and make them playable on literally any platform which can run a browser. As for tools, we need an IDE and NodeJS/NPM. For my articles, I will use the JetBrains platform for the IDE and Windows Powershell as shell.

Setting up the project

In order to setup our project, the first thing we do is use npm to create a new template:

PS > mkdir gtn |cd
PS > npm init
Wrote to C:\Users\marco_000\Projects\gtn\package.json:

{
  "name": "gtn",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

>

The above info, of course, has to be changed. So let's open the new file and change it to the following config:

{
  "name": "gtn",
  "version": "1.0.0",
  "private": true,
  "description": "",
  "devDependencies": {
    "parcel-bundler": "^1.10.3"
  },
  "scripts": {
    "dev": "parcel index.html",
    "build": "parcel build index.html"
  },
  "browserslist": [
      "last 2 versions"
  ],
  "keywords": [],
  "author": "",
  "license": "ISC"
}

First of all, I added the Parcel bundler. I chose it, because it is so darn simple to use. Just define an entry point and some configs for how things should be built, and Parcel will go figure stuff out on its own. I also told it to only care about the latest two browser versions. I don't expect gamers to use some old outdated browser on their thicc, shiny gaming machines... Last but not least, I added two scripts. The first one builds a development version of the game web app plus runs a development server which supports hot code swapping, the second one just builds the version we would want to upload to our servers.

Since Parcel uses Babel to transform our shiny next-gen code for different browsers, we also have to add a Babel configuration. Most of the time, Babel is a lot of work, however since we go for a very simple setup, all we need is the following .babelrc:

{
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "corejs": 2
    }]
  ]
}

Took me a little googling, however with Parcel, that's all you need and be done with it. Amazing. To finish the config rage, we also need a typescript config in a tsconfig.json file:

{
  "compilerOptions": {
    "alwaysStrict": true,
    "downlevelIteration": true,
    "forceConsistentCasingInFileNames": true,
    "importHelpers": true,
    "lib": [
      "dom",
      "es2018"
    ],
    "module": "esnext",
    "moduleResolution": "node",
    "outDir": "./dist",
    "removeComments": true,
    "resolveJsonModule": true,
    "sourceMap": true,
    "strict": true,
    "target": "es6"
  }
}

There's quite a bit going on here, however that's mainly enabling strict checks and modern features. Let's take a look at the options:

  • alwaysStrict strict mode prevents errors at compile time by disallowing certain unsafe or possibly undefined code situations. It's always a good idea to turn on, especially for legacy ES5 and non-module code.
  • downlevelIteration allows for things like the usage of iterable symbols in for..of loops.
  • forceConsistentCasingInFileNames enforces the same casing for a single file, which might safe you on platforms with filename case sensitivity. The IDE does a pretty great job already, though, to get it right!
  • importHelpers is needed for the transformation of certain modern syntax, like class extends ...
  • lib tells the transpiler what kind of code to expect and which library files to include in the compiled code. I want to use the latest and greatest shiny stuff, so that's what I added.
  • module tells the transpiler what module system to transform the code into. Esnext means that it will create native JS modules.
  • moduleResolution defines how modules can be included in typescript. node allows us to just import other modules and libraries and even use stuff from npm, which is a great boon.
  • outDir is the directory where the resulting source code should be saved. Usually, you want to use dist or public here, since most people use them and that's where others will look for transpiled sources.
  • removeComments speaks for itself. We don't really need comments since we have a map file in dev anyway and in prod no one would want to debug the code.
  • resolveJsonModule allows us to just import a json file and get back an object with all the fields. Convenience.
  • sourceMap enables the creation of a source map, so we can do regular code transformation, but still debug that gibberish.
  • strict enables all strict type checking options, which means safety checks at transpile-time. Big win for bug prevention and basically what makes TypeScript so great!
  • target is the target JS version we want to transpile to. ES6 aligns with our browser matrix, so we'll go for that.

Hello World

So, what we want to do is make sure that our build pipeline works flawlessly. So let's print the iconic "Hello World" message to the screen. I want the text to be printed in TS, and I want it to be styled in a separate CSS file. Let's make it a blue text. So, first of all, let's add an index.html file, which at the same time serves as entry point for ParcelJS:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Guess The Number</title>
    <link rel="stylesheet" media="screen" href="css/main.css">
    <script type="module" src="src/index.ts" defer></script>
</head>
<body>

</body>
</html>

As you can see, there's some HTML boilerplate, a link to a stylesheet and a script, which is a deferred loaded module. Deferred means that the browser starts downloading the file in the background while it builds the rest of the site. And it's source is a... TS file!?! Yes. Because that's what we want to use and Parcel should just make sure that it works in the browser. Just remember that we write code which is convenient for us developers. We have tools to make it work later on.

Next we have to.... yeah, you guessed it. Let's add a css/main.css file with the simple test-content:

body {
  color: blue;
}

That should take care of the blue color. And finally, the src/index.ts file for some coding action. It will have the very exciting content of:

document.querySelector('body').innerHTML = 'Hello World!';

Aaaand... let's build it! It will very likely install some dependencies for you. It does so in one line it updates, that's why you cannot see it in my below output.

PS > npm i .
npm WARN deprecated [email protected]1.7.7: Browserslist 2 could fail on reading Browserslist >3.0 config used in other tools.

> [email protected]0.1.13 install C:\Users\marco_000\Projects\gtn\node_modules\deasync
> node ./build.js

`win32-x64-node-10` exists; testing
Binary is fine; exiting

> [email protected]1.10.3 postinstall C:\Users\marco_000\Projects\gtn\node_modules\parcel-bundler
> node -e "console.log('\u001b[35m\u001b[1mLove Parcel? You can now donate to our open collective:\u001b[22m\u001b[39m\n > \u001b[34mhttps://opencollective.com/parcel/donate\u001b[0m')"

Love Parcel? You can now donate to our open collective:
 > https://opencollective.com/parcel/donate
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN [email protected]1.0.0 No description
npm WARN [email protected]1.0.0 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: [email protected]1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for [email protected]1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

added 736 packages from 575 contributors and audited 7627 packages in 34.29s
found 0 vulnerabilities

PS > npm run dev

> [email protected]1.0.0 dev C:\Users\marco_000\Projects\gtn
> parcel index.html

Server running at http://localhost:1234
✨  Built in 35.83s.

Click the link and... we can see the text in blue! Nice! We can work with that <3

hello_world.jpg

Oh, and don't fear the build time. The first build is always very long, but future builds will usually take no more than three seconds, which is ok imho.

Simplified Terminal

Now that we are able to produce some output, it's time to implement some basic stuff we need for our game. This time around, we need to make sure that we have basic text input and output set uhp. In order to mimic a classic terminal, we will setup a textarea as output and add an input field.

You might say "but a terminal is just one big text thingy". Yeah, but under the hood, it is made up of several streams, including the input stream and the output stream. So, in order to keep our logic simple, we will not mangle them in the interface, but get things done instead.

Out HTML body will hence look like this:

<div id="console">
    <textarea id="stdout" placeholder="Output" disabled readonly></textarea>
    <input id="stdin" type="text" placeholder="Input" autofocus>
</div>

Any good terminal also has a little prompt at the beginning of the input line, so let's also add that as a gimmick. We can just put <span id="prompt">></span> before the input and let CSS do some magic.

As for the styling, I know you all will kill me now, but let's keep it simple and go desktop only. I will even go a step further and say that implementing the mobile part is left as an exercise for the reader (you can ask me about it in the comments, though). This way, we can emulate a good old PC terminal!

We want it to be centered, 800x600 pixels and it should be dark-ish on a bright-ish background. I usually just open up the HTML in a browser at this point and start arranging the CSS. I start with the outer elements, centering everything using flexbox.

body, html {
    align-items: center;
    display: flex;
    height: 100vh;
    justify-content: center;
    width: 100vw;
}

#console {
    height: 600px;
    position: relative;
    width: 800px;
}

Then, I arrange the inner items. I use the fact that absolute positioning is still always relative to the parent with relative positioning to my advantage here. The input should be 2em high, so I arrange the rest accordingly. In order to style the input and output more easily, I add a class console-element (mostly so I don't mix in the prompt). It all results in quite simple CSS:

.console-element {
    box-sizing: border-box;
    margin: 0;
    width: 100%;
}

#prompt {
    bottom: 0;
    font-size: 1.5em;
    left: .5em;
    position: absolute;
}

#stdin {
    height: 2em;
}

#stdout {
    height: calc(600px - 2em);
    resize: none;
}

Once done, it is time to set the font and add some color to the mix. The final CSS then looks like this:

body, html {
    align-items: center;
    background-color: hsl(0, 0%, 95%);
    color: hsl(0, 0%, 15%);
    display: flex;
    font-family: sans-serif;
    font-size: 14px;
    height: 100vh;
    justify-content: center;
    width: 100vw;
}

#console {
    box-shadow: 0 0 15px 3px rgba(0,0,0,0.75);
    height: 600px;
    position: relative;
    width: 800px;
}

.console-element {
    background-color: hsl(0, 0%, 15%);
    border: none;
    box-sizing: border-box;
    color: hsl(0, 0%, 95%);
    margin: 0;
    padding: .5em;
    width: 100%;
}

.console-element:focus {
    /* bad chrome, don't outline */
    outline: none;
}

#prompt {
    bottom: 0;
    color: white;
    font-size: 1.5em;
    left: .5em;
    position: absolute;
}

#stdin {
    height: 2em;
    padding-left: 2em;
}

#stdout {
    border-bottom: 1px inset white;
    cursor: default !important;
    height: calc(600px - 2em);
    resize: none;
}

GUI? DONE! Let's add some code (finally)~

Input, Processing, Output

In your regular console application, execution starts at the top and works its way to the bottom. JavaScript is the same. Except, it's not. In the case of IPO, JS is all async. Most of the APIs do their stuff in the background, so the browser can have better control over rendering and a website feels more responsive. That's what will happening to our application, too. We cannot simply wait until a user inputs something and then continue. JS does that in the background for use, so we will have to create an event-based IPO. Good thing we have have simple elements :)

Let's start even simpler, though. Let's output some hello world to our console on start:

const stdin: HTMLInputElement = document.getElementById('stdin') as HTMLInputElement;
const stdout: HTMLTextAreaElement = document.getElementById('stdout') as HTMLTextAreaElement;

stdout.innerText +=
    'Welcome to Guess The Number!\n' +
    'Please use the below input field to reply :)\n' +
    'First of all, what\'s your name?\n';

And npm run dev. Open the website, take a look at the result. Looks like a game to me πŸ˜ƒ Note: I made the observation, that some browsers need the value field instead. We will abstract that in just a moment

game_start.jpg

One of the important things I cam to find out is that making certain things re-usable is very important, and abstracting away low-level things can make business-logic clean. So let's abstract our console away so that we can use it just like a regular sync C-like console.

In order to do so, we can make use of async/await. Let's wrap the game logic into async and write a promise-based console API!

class MyConsole {
    // todo
}

const stdin: HTMLInputElement = document.getElementById('stdin') as HTMLInputElement;
const stdout: HTMLTextAreaElement = document.getElementById('stdout') as HTMLTextAreaElement;
const myConsole = new MyConsole(stdin, stdout);

(async () => {
    // let's also directly convert our welcome text to future-API
    myConsole.println('Welcome to Guess The Number!');
    myConsole.println('Please use the below input field to reply :)');
    myConsole.println('First of all, what\'s your name?');
})().catch(myConsole.println);

First of all, let's create the constructor. We can keep the class very modular, if we accept the input and output as parameters.

    constructor(stdin: HTMLInputElement, stdout: HTMLTextAreaElement) {
        this.stdin = stdin;
        this.stdout = stdout;
    }

We already know how to do output, so println() (print line) is rather straight forward!

    /**
     * Write output to console
     */
    print(str: string) {
        // Insert text into textarea
        this.stdout.innerText += str;
        this.stdout.value += str;

        // scroll down to the bottom, so the newest line is always visible
        this.stdout.scrollTop = this.stdout.scrollHeight;
    }

    /**
     * Write a line of text to the console
     */
    println(str: string) {
        this.print(str + '\n');
    }

While we are at it, let's also add a convenience method to clear the whole terminal

    /**
     * Remove all output
     */
    clear() {
        this.stdout.innerText = '';
        this.stdout.value = '';
    }

That was easy, right? Now we can write to the console whatever we want. now, for the hard part. Input. We want to

  • Call a method, which returns a Promise
  • The Promise should be resolved once input is available
  • A standard terminal uses streams, which can also contain old input. We want to emulate that.

Hence, our console class is a real time machine. It has to be able to handle past and future input. At the same time, it is only allowed to return a Promise, and a user of the API should not have to care about it. How do we do that?

We can do that by using a buffer and a drain. The logic is: We store input in a buffer and requests to read in a drain. Buffers will be arrays of string, because they contain the input. Drains will be arrays of (unresolved) Promises, because they contain the read-requests from user-code. Once input is entered, the logic will have to check if there are drains which are waiting for input, and, if not, store the input in the buffer. At the same time, on a read request, the code should check if there already is something in the buffer, else add the Promise to the drain. Sounds complicated? Maybe some code will help! First of all, let's add an event listener for stdin in the constructor:

    constructor(stdin: HTMLInputElement, stdout: HTMLTextAreaElement) {
        this.inputBuffer = [];
        this.inputDrain = [];
        this.stdin = stdin;
        this.stdout = stdout;

        // React to every key-input in the input element
        this.stdin.addEventListener('keydown', eve => {
            // Check if ENTER button was pressed
            if (eve.which !== 13) {
                // If not, there is nothing to process (yet)
                return;
            }

            // Fist, check if someone wants to read
            if (this.inputDrain.length > 0) {
                // If there is a read-request, fulfill it and remove it from the drain array
                this.inputDrain.shift()(this.stdin.value);
            }
            else {
                // If not, write the input line to the buffer
                this.inputBuffer.push(this.stdin.value);
            }

            // Last, but not least, clear the input
            this.stdin.value = '';
            this.stdin.focus();
        });
    }

Then, the counterpart, which is the read method we want to call from the game logic:

    /**
     * Read line from input
     */
    readln(): Promise<string> {
        // First, check if input is already available
        if (this.inputBuffer.length > 0) {
            // If there is an input line, return a fulfilled Promise and remove the line from the buffer array
            return Promise.resolve(this.inputBuffer.shift() as string);
        }

        // If not, return a Promsise, which adds its resolver to the drain array
        return new Promise(resolve => this.inputDrain.push(input => resolve(input)));
    }

Yes, we could have used a library. However, the above implementation is ca. 50 lines of code long, and fulfills all our needs. Do we really need to add baggage we never use with a feature-rich library intended for general use?

Game Logic Time

Did you know? We are at a point, where we can write SYNCHRONOUS input-processing-output. In a loop. We could write any console application as though this would be C code. No problematic JS async thingies forcing us to start event and callback-wars. We just solved a very hard part about gamedev in JS. Getting rid of async, while still taking advantage of it.

So, let's review how we expect guess the number to work

  1. Computer generates number between 1 and 100
  2. User has to input a guess
  3. Computer tells user if their number is right, or greater or smaller
  4. Either game ends, or go to 2)

That's all. Let's write an algorithm. Do you see the loop in there? 4.->2.? Yep, we will write a loop, all right.

(async () => {
    // Prepare the output
    myConsole.clear();

    // Say welcome
    myConsole.println('Welcome to Guess The Number!');
    myConsole.println('Please use the below input field to reply :)');
    myConsole.println("First of all, what's your name?");

    // Let's store the player name and use our shiny console class. No worries about implementation here.
    const playerName = await myConsole.readln();

    // Say hello - just to check early if the input really works 
    myConsole.println(`Hello there, ${playerName}!`);

    // Create random number between 1 and 100, using the remainder-operator
    const number = (Math.random() * 1000 % 100 + 1) |0;

    myConsole.println('I thought of a number between 1 and 100.');

    // Make space for a user guess
    let guess: number;
    // Here's the loop for 2., 3. and 4. - it's an infinite loop with break-criteria in its body
    do {
        myConsole.print('Take a guess: ');
        // Read the guess as user input
        guess = parseInt(await myConsole.readln());
        // Since the user cannot see their number anymore after the input is cleared, let's write it to the output 
        myConsole.println(guess.toString());

        // Compare the guess to the random number and give a tip
        myConsole.println(
            guess > number
                ? 'My number is smaller!'
                : guess < number
                   ? 'My number is greater!'
                   : `Congratulations, ${playerName}! My random number is ${number}!`
        );
    } while (guess !== number);

    // disable the input, the game is over!
    stdin.setAttribute('disabled', '');
})().catch(myConsole.println);

Wohaaa, not so fast there! That's really all the code? What's a remainder-operator? Also, how is this code not blocking?

Okey, since the actual game logic is very very simple, now, I went ahead and first delivered some code. Yes, that's the whole game. However, let's walk through it a little bit.

First of all, since we use async-await, every time we await a promise (input), the code execution is paused and the browser can do its thing. That's why this code does not block and a user can do input. Once the player hits [ENTER], execution resumes, because the Promise is resolved. We have an await in the loop, so the code won't block even there.

Last, but not least, about the generation of the random number. To be honest, there are many ways to calculate a random number in a range in JS. However, from my old math days, I remember the modulo operation quite fondly, so that's what I decided to use. Lets say A and B are numbers, for example 5 and 10. If we do A modulo B, then A will be divided by B. Then, everything which would be used for the floating-point part is used as result integer. For 5 modulo 10, 5/10 would be 0 with 5 being the result. It also works with greater As. Let A be 42. 42 modulo 10 means that 4/10 = 4.xxx. We can just calculate 4*10=40 and substract that from A. The result is 2.

So, Math.random() * 1000 generates a random number which can have an integer part greater zero. Since the integer part might be greater than 100, which is our upper limit, I use the remainder-operator (%), which is nearly identical to modulo in math, to only get what we have too much. The result can never be >=100 or <0. That explains Math.random() * 1000 % 100. Next, if we take a look at the range, we see that the result of this operation is 0..99. In order to fix that, we have to add 1: Math.random() * 1000 % 100 + 1. Last but not least, the bitwise operation at the end is a simple way to cut off the decimal part, leaving only an integer. After all, guessing a decimal number is like guess-the-number, but annoying. That's not fun. Games should be fun.

How To Distribute The Game?

After building the code using npm run build, the distributable source is located in the folder /dist. It contains one html file, one css file, one js file and several map files. Don't worry about the strange names. Parcel made sure everything works out for you. Parcel actually did you a little favor, embedding hashes in the name, which forces all browsers to reload changed assets after uploading a new version. For more complex applications, that might be a problem, but for us, that's perfect and easy.

If you open the JS file, you will be greeted by a lot of gibberish. Parcel did a number there and now it does not only contain our code, but also polyfills and other stuff, plus optimization.

output_gibberish.jpg

The important part is, that you can just upload the contents of the dist folder to your webspace, and people will be able to navigate to your game. If you want to distribute the files offline, just tell people to double-click index.html and a browser will show them the web application.

Little fun fact at the end: The resulting code files are 45.7KB in size (not counting map files). The Rust version of this game created an executable with a whopping 74KB... after heavy size optimizations. Does this mean that web applications are smaller? No, I would not say that, however, an executable has a lot of overhead, which is very visible in small applications. If we created a bigger game (like Quake), it's quite likely that the executable would be smaller than the generated web app. Also, let's not forget that we need a JSVM in order to execute the web app, which usually is several MBs in size...


In the next part, we will use this simple game in order to understand more complex games, and then create something a little more practical than just guessing numbers. I really hope you like this article and learned a few things from it. Leave your opinion and any questions below and I will try to answer anything which comes up πŸ™ƒ


Unbezahlte Werbung durch Nennung und Verlinkung von Personen, Organisationen oder Unternehmen.

Write your comment…

I'd suggest trying to avoid using while(true) and break -- it reeks of spaghetti code jump-style.

You could just do-while there. As someone weaned on assembly and pascal, the do/while (what Pascal calls repeat/until, and in ASM is the cornerstone of all efficient loops) structure is just closer to how the hardware works and feels more intuitive the lower-level you go. As a construct, while (true) is just a wasted boolean comparison in the loop.

do {
    myConsole.print('Take a guess: ');
    guess = parseInt(await myConsole.readln());
    myConsole.println(guess.toString());
    myConsole.println(
        guess > number ? 'My number is smaller!' : (
            guess < number ?
            'My number is greater!' :
            `Congratulations, ${playerName}! My random number is ${number}!`
        )
    );
} while (guess !== number);

Also, ternary operators are your friend. If every single result is just going to println, you only need one println.

Show all replies

Marco Alka Don't feel stupid. It's something I've noticed the past few decades of programming and it's all about background and exposure. I'm actually writing a rather lengthy blog post/article on the topic of such things, hope to have it done by saturday

People who write in C, coming from C backgrounds, simply don't think in terms of do/while. They will jump through all sorts of crazy hoops of recursive function calls, while(true), even trying to get jump/goto labels into their compilers because even if they're aware "do" exists, they just aren't primed to think that way. All they've ever seen and all they've ever done is while(){} that so they never even think of do{}while();

Because languages like PHP, JavaScript, Java, and so forth are C syntax -- for all intents and purposes nothing more than C tarted up in a slinky dress with theatrical makeup and 4" stilleto CFM pumps -- everyone using those languages just copies that model and mindset.

But those of us who learned Assembler or Wirth syntax languages think in terms of "the condition is at the end of the loop". It's why "for" loops feel counterintuitive to me even after some three and a half decades of using them... Since I still in my head think "set the counter to the number of loops, then decrement until zero".

Which is why this:

var i = 100;
do {
    // whatever
} while (!(--i));

Still feels more "correct' in my mind than a for loop.

Or something as simple as DOM walking, where if/do/while is the more powerful way, even if you do need a recursive call. Let's say you wanted to walk a TR element's node tree with a callback for all the cells inside it.

It's laughable how many people would do this:

function nodeAllCells(e, callback) {
    for (var i = 0; i < e.cells.length; i++) {
        callback(e.cells[i]);
    }
}

But then even said for loop / element index is inefficient when it comes to objects or nodelists. Czech this out:

function nodeAllCells(e, callback) {
    for (var i = 0, cell; cell = e.cells[i]; i++) {
        callback(cell);
    }
}

That's actually the fastest way to iterate through a nodeList, because it removes an unnecessary comparison. So long as all the items in an array-like are not loose-false, it is faster in JS to iterate through them by assignment, since assignment past the end of the array-like is null... but I seem to be the only person who still does that.

But then try this on for size:

function nodeWalkCells(e, callback) {
    if (e = e.firstElementChild) do {
        callback(e);
    } while (e = e.nextElementSibling);
}

Whilst yes, it's more code, this one runs way faster, because JavaScript array-likes are always slow since they aren't true arrays, they're pointered lists that to index into them, you have to iterate past every single entry BEFORE the one you're looking for each and every access! Going to the DOM structure to find the first one then loop through all the siblings cuts out that middle-man.

Same with this obsession with Array.forEach and Object.forEach, particularly with arrow functions we've been seeing. It's C style thinking that forgets the fact you just introduced function overhead AND extra stack assignment to the equation. They are not saving you any real code worth mention, they are not making the resultant code any clearer, etc, etc, etc...

I keep seeing people operate on objects thusly:

Object.keys(this).forEach(key => {
    const value = this[key];
    // whatever
});

Which is more cryptic, slower, induces memory thrashing with the block level stack release, and is actually more code!

for (var key in this) {
    var value = this[key];

In fact because the variables aren't locked / released at the block scope, it means LESS stack thrashing since you only allocate as needed (or even better lifting turns it into forward declaration) and a single stack release. Turning that into x86 assembler means a simple "ret x" where x is the number of bytes allocated to function-local variable space on the stack.

But don't try to tell the ECMAScript 6 fanboys that... which like everything else hot, trendy, and steps backwards in mindset they have plenty of lame excuses for, that hold water like a steel sieve.

Basically though, you're thinking while{} because that's all you ever see in C syntax code. Not because there aren't better ways, but because the concepts of where and when to use them -- much less their existence -- simply isn't discussed. Just like how the condition operator in a for loop can accept ANY condition. Just like how "for/in" or "for/of" in most cases is superior in clarity to .foreach(->).

Either people don't know they exist because they've never seen it in a codebase, or avoid it out of unfounded fears made up by those who couldn't grasp them. (see the bizzaro claims about how "unreliable" for/in is.)

Reply to this…

Hashnode is a friendly and inclusive dev community.
Come jump on the bandwagon!

  • πŸ’¬ Ask programming questions without being judged

  • 🧠 Stay in the loop and grow your knowledge

  • πŸ• More than 500K developers share programming wisdom here

  • ❀️ Support the growing dev community!

Create my profile

Really thorough tutorial! A couple of points, though:

  • The randomization algorithm is too complex for something too simple; this might be a better one: Math.floor( Math.random() * 100 ) + 0 where 100 is the upper bound and 0 is the lower bound.
  • With a third-party package, you can use streams in the browser. This is much better since as the game grows, the text will be way too much and can cause an IO block.
  • You say JavaScript is all async. That is not the case. It is very synchronous; you can use the Event-Loop (setTimeout for example, in the browser) to do async tasks. However, this claim is wrong. The reason why we have these async functions is because JavaScript is sync and some tasks are blocking.
  • For the infinite loop, I'd suggest using while( true ) for nothing else but readability.
  • Instead of escaping ' in the line First of all, what\'s your name?, you can use double-quotes which allow single quotes to co-exist in the string: "First of all, what's your name?"
  • For the if-then-else ladder, using an elseif block is much better than isolated conditions.

The Author Card

Marco Alka's photo

Marco Alka

Software Engineer, Hobby Game Developer

Appreciations

5.0K

Location

GERMANY

Joined

Apr 12, 2016

Hashnode Evangelist