Faces Forest Logo, returns to index.

Article


Let's Make a Game

By: Face Rizzi

Tags: design, software_development, article, tutorial, basics

Revision: 2

Published: 5/31/2022

Introduction

Starting out, I wanted to make a brief set of tutorials someone could use to make an executable file that would let them host a simple game. When I was in college, I greatly appreciated many games made in Rpgmaker and rags, but for a number of years I've had the impression that someone could make something comparable in JavaScript that'd be more future-proof. Additionally, while they allowed a user to download a commercial dependency onto their operation-system, that chain was visible and gave the impression of minimal configuration.

At the outset I wrote a long form article that... well. I'll be frank. The full extent of my ADHD is on display in the original draft. In the intervening time I had a quick think, and decided I best split the work into two parts.

One part ought to be a convenient technical document that could be used in any context. While making a game of any kind is a good goal, making the kind of games I intend to, for the community I'm serving, is more of a hobby or calling. The behavior and opinions a sexual culture invites distract from the work. My use (or abuse) of the software for an edge case shouldn't suggest that it isn't a generic and effective tool

The other part I could fill with jokes, cursing, and my often contradictory opinions. It might give some people a bit of entertainment, a bit of insight into things a developer likes and a bit of hope to see a fellow human sweat and bleed. The biggest takeaway from that section is why I'm asking a person to do it this way.

Goal of this section

The goal of this section is to show a user that they can set up a primitive development workflow with Electron, JavaScript and HTML. This workflow has a few steps:

  1. Author Markdown.
  2. Insert it into the index page for the default electron app.
  3. Perform tests on the application to verify it is correct.
  4. Publish.

This simple view of things is accurate, it is ambiguous to the extent that it isn't so useful. It is also for a very early development step where you are verifying that your process simply can produce output.

Level of experience

Basic competence with a computer is needed. In particular a user needs to understand how to use a terminal whether under Windows or Linux. The user should understand how to use an application called a package manager, as well as the basics for reading, writing, and removing files and directories. A potential user they should understand how to use the native help facilities in a terminal.

At this stage, a formal training in computer is not needed. It is helpful, and rewarding. But what is needed is a willingness to follow instructions, and a willingness to check, and check again.

Step 1: Install the dependent programs

As a package manager and its facilities are not available by default on Windows, you will likely need to get one. At the time of this writing an experimental facility is available, but it's capacity to be effective is unknown.

There are two good choices, Chocolatey and Scoop. I use Scoop. On Unix-like systems, package managers are implementation dependent but standard.

Step 2: Install NPM and NodeJS

NodeJS is a JavaScript interpreter. Node was an effort to allow JavaScript to work on a file system using an event driven architecture.

This architecture is not efficient, relative to what can be done with C++. It is however, effective, with a wide user base and thousands of software applications written. What it loses in efficiency it makes forward in ease of use. First, it runs a dialect of JavaScript, so a web developer can use that experience and not have to learn a new language. Second it is interpreted, so operations can be tested informally directly without having to wait for a compilation step.

Node Package Manager is used for basic package setup and installation.

Step 3: Get comfortable

By which I mean, get your native code text editor setup for JavaScript. For a good time, I recommend Visual Studio Code. The exact working tool is the users choice, but it should be of that form.

No serious software project should be done without version control software. The capabilities it brings to backing up and restoring projects alone are indispensable. I recommend git.

Install the required programs with NPM

The system we're setting up will need the following programs saved as development dependencies.

The form of the installation process is to initialize a package.

This is normally done with

npm init

However, I'm using WebDriver for testing. The expedient method for doing this is to run

npm init wdio .

Within an existing empty directory or an existing node project.

After wards installation of packages is done with:

npm --save-dev {{your software}}

The programs you will certainly need are:

  1. Electron - As an application framework to develop and deploy software.
  2. Electron-forge - For deployment specifically.
  3. Markdown-it - Content Creation
  4. Nunjucks - Templates

A program that will get optionally installed with WebDriver, as a testing framework, is mocha. This is the default choice, and a good one at that.

Step 4: Setup individual elements

The next step is to code in the functionality that'd allow a user to make code.

Markdown-it

It is easiest to start at the Markdown-it package. Create a file at the root of the node package called "generate_HTML.js". Inside goes:

const { readFile, writeFile, unlink} = require("node:fs")
const path = require("node:path")
const md = require("Markdown-it")
const Nunjucks = require("Nunjucks")
module.exports={
makeNunjucks:createNunjucksParser(),
createNunjucksParser,
makePath:makePath,
generateMarkdown,
buildNunjucksWithMD:mdNjkWeld
}
/**
* @name makePath
* @description Takes a number of pathes.
* Returns file system specific representation of __dirname/parts/of/path
* Ignores the ./ and / in a file string.
* @param  {...any} paths
* @returns {path.ParsedPath} system filename as NodeJS ParsedPath object,
* from which a system agnostic string can be constructed with: path.format.
*
*/
function makePath(...paths){
const pthstr = path.join(__dirname, ...paths)
return path.format(path.parse(pthstr))
}
/**
*
* @param {string} md_file A path string in unix or
* @param {string} output_location
* @param {callback} callfn A callback with no arguments, used to continue working once the task of writing the Markdown has been completed.
* @param {Markdownit.ConfigureOptions} mdRenderOptions Options to control the Markdown renderer. Note: default here renders HTML elements. See:
* @returns {}
*/
function generateMarkdown(md_file, output_location, callfn, mdRenderOptions={HTML:true}){
const mdRenderer = new md(mdRenderOptions);
return readFile(makePath(md_file), (err, md_txt )=>{
if (err){
throw err;
}
const md_res = mdRenderer.render(md_txt.toString('utf8'))
return writeFile(makePath( output_location ), md_res, (err)=>{
if(err){
throw err;
}
if (callfn){
callfn()
} else {
return;
}
})
})
}

Of note- it is necessary to set HTML:HTML true to output actual HTML elements with Markdown. If someone supposes a use case of generating a visual novel, or text based choose your own adventure, it is useful to retain the ability of Markdown to directly create HTML. If the Markdown parser is not exposed to beyond the user's machine this practice is safe. Otherwise, it is simply a function that takes a path relative to the node server and spits out HTML from Markdown.

Nunjucks

I used the same module for my Nunjucks code.

/**
*
* @param {string} template_sources Template sources. By default: template/Nunjucks
* @param {Nunjucks.ConfigureOptions} Nunjucks_configuration An optional Nunjucks configuration object.
* @returns {function (string, object, string, callback)}
* @param {callback} callfn A callback with no arguments, used to continue working after the Nunjucks has been written.
* Returns a function for making Nunjucks templates.
* Takes the name of the input template, relative to template_sources, context of the Nunjucks template, and the output that.
*/
function createNunjucksParser(template_sources="templates/Nunjucks", Nunjucks_configuration={autoescape:false, noCache:true}){
const env = (Nunjucks_configuration === undefined) ? Nunjucks.configure(makePath(template_sources)): Nunjucks.configure(makePath(template_sources), Nunjucks_configuration)
//build a standard environement
return (template_name, context, output_path, callback)=>{
env.render(template_name, context,
(err, res)=>{
if(err){
throw err;
}
return writeFile(makePath(output_path), res, (err)=>{
if(err){
throw err;
}
if(callback){
return callback()
} else{
return
}
})
});
};
}
/**
*
* @param {string} njk_name Name of the Nunjucks template being written.
* @param {string} output_location Filepath to where the final output will exist
* @param {string} md_file File path to the Markdown file being written.
* @param {string} final_name final name of the content to be written, which will be appended to output_location.
* @param {string} md_content_tag The key with which the Markdown content will be stored within the Nunjucks variable scope.
* @param {object} njk_content a Nunjucks object representing the Nunjucks variable scope.
*/
function mdNjkWeld(njk_name, output_location, md_file, final_name, md_content_tag="md_content", njk_content={}){
let fname = path.join(output_location, path.basename(md_file)+".HTML")
generateMarkdown(md_file, fname, ()=>{
readFile(path.join(path.basename(md_file)+".HTML" ), function(err,data){
if (err){
throw err
}
const parser = createNunjucksParser("./templates/Nunjucks")
njk_content[md_content_tag] = data.toString()
parser( njk_name, njk_content,path.join(output_location, final_name),()=>{
unlink(makePath(fname), function(err){
if(err){
throw err
}
})
})
})
})
}

The workflow for a Nunjucks template engine is a bit different. Nunjucks creates a context around a file directory with a configuration. Like Markdown-it, it is necessary to downgrade the security to allow for efficient content creation, and to disable caching so that templates are recreated on demand instead of stored for use.

The easiest way to make a future-proof configuration that only uses a single set of resources is to hide the creation of the parser in a scope and return a function that uses it. This provides the packages default Nunjucks parsing function in package.exports.

mdNjkWeld is a simple convenience function that automates the process of generating a Markdown file, reading it into a Nunjucks file and writing it. Its name is changed in exports to something longer and more legible to the end user.

Verifying the setup steps with data

The input to verify that these tools were correct was single Markdown file and a single Nunjucks template.

The Markdown file simply shows it writes Markdown and HTML. To manage Markdown in the long term, the directory templates/Markdown was created.

Inside the file hello.md:


### Hello World
From a Markdown template!
<p class="special">
This is a special purpose HTML class.
Markdown treats *block* level HTML as HTML.
</p>
Html is valid in Markdown, it is simply
that Markdown lets us do things in a convenient
manner.
Note that Markdown doesn't
*evaluate HTML content!*

There's another directory Nunjucks content in templates. The file at ./templates/Nunjucks/primary.njk contains the following code:

 <!DOCTYPE HTML>
<HTML>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
<title>First App</title>
</head>
<body>
<h1> Hello World</h1>
<p>Ensure that electron works!</p>
<p>
We are using Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
and Electron <span id="electron-version"></span>.
</p>
<p>Ensure that Markdown works!</p>
{{md_content}}
</body>
</HTML>

This is near word for word what was is on the basic electron getting started page.

The main addition here is to include a variable-expansion. An entire Markdown file's worth of arbitrary text can be inserted explained there.

Getting output

I used NodeJS for this directly. On the command line:

node
let x = require("./generate_HTML")
x.buildNunjucksWithMD("primary.njk", "./", "./templates/Markdown/hello.md", "test2.HTML")
.exit

Sees the creation of this file in the root of the package.

<!DOCTYPE HTML>
<HTML>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
<title>First App</title>
</head>
<body>
<h1> Hello World</h1>
<p>Ensure that electron works!</p>
<p>
We are using Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
and Electron <span id="electron-version"></span>.
</p>
<p>Ensure that Markdown works!</p>
<h3>Hello World</h3>
<p>From a Markdown template!</p>
<p class="special">
This is a special purpose HTML class.
Markdown treats *block* level HTML as HTML.
</p>
<p>Html is valid in Markdown, it is simply
that Markdown lets us do things in a convenient
manner.</p>
<p>Note that Markdown doesn't
<em>evaluate HTML content!</em></p>
</body>
</HTML>

Please note that the exercise of building a command line interface is reserved for a later tutorial. A discussion of formatting and minification is in the opinion section.

Setup Electron

For the purposes of this application and for brevity it was simply necessary to demonstrate that an executable could be created. The entire quick start code of the Electron Project was tested and found effective.

The only real deviation from that step was to change the target of window onload, so it pointed at "test2.HTML" instead of "index.HTML". I will at some point return to the web convention of using the file "index.HTML" as the root of my work, but for now I wanted to unambiguously show this is a test.

Build an app

Per the standard instructions, electron forge works out of the box, as demonstrated. Running npx electron-forge import sets the project up and from then on, running npm run make builds the application into an executable.

Running that executable produced the following window:

First Electron Window

Configuring the test environment

The last stage was setting up the proper test environment. At the installation step of WebDriver most of the configuration was done. The other steps are clearly illustrated out with a few exceptions.

The first is that the electron-chromedriver doesn't always agree with the installation of WebDriver. I ended up manually installing the correct version of chromedriver for my version of Electron, based on the output of some error messages. I used the command

npm install --save-dev chromedriver@100.00.00

But I caution the reader not to expect it to work consistently. The release page of electron documents which version of chromedriver is required for a particular version of Electron.

Second, is that there isn't a great test file running against the default electron app. Per convention tests are located in ./test/specs, and I created a file basics.js to demonstrate a few things:

const assert = require("node:assert")
describe('Demo Test', () => {
it('should be able to find h1', async () => {
const elem = await $('h1')
let txt = await elem.getText()
assert.equal( txt, "Hello World")
});
it('should be able to tell what the node version is', async ()=>{
const elem = await $('#node-version')
const  txt = await elem.getText()
assert.ok(txt)
console.log( `Got ${txt}; actual node version ${process.versions.node}` )
})
it('should be able to tell what the chrome verion is', async ()=>{
const elem = await $('#chrome-version')
const  txt = await elem.getText()
assert.ok(txt)
console.log( `Got ${txt}; actual chrome version ${process.versions.chrome}` )
})
it('should be able to tell what the electron version is', async ()=>{
const elem = await $('#electron-version')
const  txt = await elem.getText()
assert.ok(txt)
console.log( `Got ${txt}; actual electron version ${process.versions.electron}` )
})
});

Note the console print statements. This is more or less a standard mocha test, but these print statements show some notable behavior on the part of electron - the node version it uses appeared to be particular to it and did not match the one I used on my operating system.

Running tests can be done as stated in the electron documentation, once the configuration steps there are performed and the appropriate chromedriver is installed.

Finding console log statements in output is another matter entirely. WebDriver is rather verbose. I suggest redirecting the output to a text file with the >> operator available in many terminals.

My version of WebDriver installed an expedient script command wdio, so I just needed to type npm run wdio to get the same effect. If you wish to put redirect to a text file as I suggest above the entire command is npm run wdio >> results.txt. There was one more minor optional configuration change I made:

I changed the reporter so that I could see the result of redirected output. In the file wdio.conf.json, I have:

reporters: [
['spec', {
symbols:{
passed: '[PASS]',
failed: '[FAIL]'
}
}]],

This setup allows me to use the redirection operator npm run wdio >> result.txt and still be able to read if a test passed or failed. When I didn't set that up I was not able to see redirected checkmark characters in my text editor.

Conclusion and next steps

At the conclusion of part 1, you have a small HTML file you can inject HTML into. And you can package that as an executable. At the risk that no one reads the opinionated section of the code:

This is a program capable of everything a browser is capable of, plus performing file input and output. It has an end-to-end testing framework that will operate like Selenium, allowing you to select and click on elements on the application window. Any capability you have in a browser you have here.

What's more important is what is missing. There isn't 3,000 or so companies making demands of Google, including what content their content should be paired with. You don't have to bow to their expectation that their content is to appear at all times, or the expectation that the support of their content has any bearing on how you secure the application.

There is not a monthly fee to use a platform as a service. There is an idea that the software needed to host applications is all mathematics, a universal language that is free - so software can or should be free. The reality is that time and space to host software costs money and always will. There is not the need to measure a community size and estimate a number of servers to cover that - and figure out a way to either break even on the fee for that.

There's no need to adjust your look and feel based off of any browser other than the Chromium version packaged with electron. So this gets the ease factor of web development without dealing with some of the nasty barriers for entry of it.

There are game engines implemented in javascript, available for free that a person could use to control the behavior of an application. And databases can be hosted on the end users operating system. It's my belief that this is a solid foundation to start a user building applications.

Next:

[User interface design]

And the opinion section for this part.


A link to Face Rizzi's subscribestar account

This is my subscribestar page. You may go directly there because they believe in freedom of speech. That is greatly appreciated.

I have a Ko-Fi page. Out of respect for their terms and conditions, I should not paste a direct hyperlink. I respect that they need to maintain a good relationship with Stripe and Paypal. If you search their web page for me you will see my logo, my link ends in facerizzi12818.