From 2d7a1b2a367d54e8675bbffa2e493c54de77beea Mon Sep 17 00:00:00 2001 From: Alexander Wunschik Date: Sat, 19 Mar 2022 16:48:30 +0100 Subject: [PATCH] show terminal header with link to sources --- .../javascript/WebTerminal/HtmlTerminal.css | 69 +++++++ .../javascript/WebTerminal/HtmlTerminal.js | 190 ++++++++++++++++++ .../javascript/WebTerminal/terminal.html | 116 +++++++++++ .../javascript/WebTerminal/terminal_tests.mjs | 30 +++ 00_Common/javascript/common.mjs | 21 ++ 00_Utilities/build-index.js | 46 +++-- 00_Utilities/javascript/style_terminal.css | 11 +- .../javascript/rockscissors.html | 10 - .../javascript/rockscissors.js | 107 ---------- .../javascript/rockscissors.mjs | 117 +++++++++++ 78_Sine_Wave/javascript/sinewave.html | 16 -- 78_Sine_Wave/javascript/sinewave.js | 22 -- 78_Sine_Wave/javascript/sinewave.mjs | 18 ++ HOW_TO_RUN_THE_GAMES.md | 16 +- index.html | 2 +- 15 files changed, 616 insertions(+), 175 deletions(-) create mode 100644 00_Common/javascript/WebTerminal/HtmlTerminal.css create mode 100644 00_Common/javascript/WebTerminal/HtmlTerminal.js create mode 100644 00_Common/javascript/WebTerminal/terminal.html create mode 100644 00_Common/javascript/WebTerminal/terminal_tests.mjs create mode 100644 00_Common/javascript/common.mjs delete mode 100644 74_Rock_Scissors_Paper/javascript/rockscissors.html delete mode 100644 74_Rock_Scissors_Paper/javascript/rockscissors.js create mode 100644 74_Rock_Scissors_Paper/javascript/rockscissors.mjs delete mode 100644 78_Sine_Wave/javascript/sinewave.html delete mode 100644 78_Sine_Wave/javascript/sinewave.js create mode 100644 78_Sine_Wave/javascript/sinewave.mjs diff --git a/00_Common/javascript/WebTerminal/HtmlTerminal.css b/00_Common/javascript/WebTerminal/HtmlTerminal.css new file mode 100644 index 00000000..54eac8d3 --- /dev/null +++ b/00_Common/javascript/WebTerminal/HtmlTerminal.css @@ -0,0 +1,69 @@ +:root { + --terminal-font: 1em "Lucida Console", "Courier New", monospace; + --background-color: transparent; + --text-color: var(--text); + --prompt-char: '$ '; + --cursor-char: '_'; +} + +/* Basic terminal style. + * If you wan t to overwrite them use custom properties (variables). + */ +.terminal { + font: var(--terminal-font); + background-color: var(--background-color); + color: var(--text-color); + + overflow-y: scroll; + width: max-content; +} + +/* The terminal consits of multiple "line" elements + * Because sometimes we want to add a simulates "prompt" at the end of a line + * we need to make it an "inline" element and handle line-breaks + * by adding
elements */ +.terminal pre.line { + display: inline-block; + font: var(--terminal-font); + margin: 0; + padding: 0; +} + +/* The "terminal" has one "prompt" element. + * This prompt is not any kind of input, but just a simple + * with an id "prompt" and a + */ +@keyframes prompt-blink { + 100% { + opacity: 0; + } +} +.terminal #prompt { + display: inline-block; +} +.terminal #prompt:before { + display: inline-block; + content: var(--prompt-char); + font: var(--terminal-font); +} +.terminal #prompt:after { + display: inline-block; + content: var(--cursor-char); + background: var(--text); + animation: prompt-blink 1s steps(2) infinite; + width: 0.75rem; + opacity: 1; +} + + +/* Terminal scrollbar */ +::-webkit-scrollbar { + width: 3px; + height: 3px; +} +::-webkit-scrollbar-track { + background: var(--background-color); +} +::-webkit-scrollbar-thumb { + background: var(--text-color); +} diff --git a/00_Common/javascript/WebTerminal/HtmlTerminal.js b/00_Common/javascript/WebTerminal/HtmlTerminal.js new file mode 100644 index 00000000..4a095c87 --- /dev/null +++ b/00_Common/javascript/WebTerminal/HtmlTerminal.js @@ -0,0 +1,190 @@ +/** + * @class HtmlTerminal + * + * This class is a very basic implementation of a "terminal" in the browser. + * It provides simple functions like "write" and an "input" Callback. + * + * @license AGPL-2.0 + * @author Alexaner Wunschik + */ +class HtmlTerminal { + + /** + * Input callback. + * If the prompt is activated by calling the input function + * a callback is defined. If this member is not set this means + * the prompt is not active. + * + * @private + * @type {function} + */ + #inputCallback = undefined; + + /** + * A html element to show a "prompt". + * + * @private + * @type {HTMLElement} + */ + #$prompt = undefined; + + /** + * Constructor + * Creates a basic terminal simulation on the provided HTMLElement. + * + * @param {HTMLElement} $output - a dom element + */ + constructor($output) { + // Store the output DOM element in a local variable. + this.$output = $output; + + // Clear terminal. + this.clear(); + + // Add the call "terminal" to the $output element. + this.$output.classList.add('terminal'); + + // Create a prompt element. + // This element gets added if input is needed + this.#$prompt = document.createElement("span"); + this.#$prompt.setAttribute("id", "prompt"); + this.#$prompt.innerText = ""; + + //TODO: this handler shouls be only on the propt element and only active if cursor is visible + document.addEventListener("keyup", this.#handleKey.bind(this)); + } + + /** + * Creates a new HTMLElement with the given text content. + * This element than gets added to the $output as a new "line". + * + * @private + * @memberof MinimalTerminal + * @param {String} text - text that should be displayed in the new "line". + * @returns {HTMLElement} return a new DOM Element

+   */
+  #newLine(text) {
+    const $lineNode = document.createElement("pre");
+    $lineNode.classList.add("line");
+    $lineNode.innerText = text;
+    return $lineNode;
+  }
+
+  /**
+   * TODO
+   * 
+   * @private
+   * @param {*} e 
+   */
+  #handleKey(e) {
+    // if no input-callback is defined 
+    if (!this.#inputCallback) {
+      return;
+    }
+
+    if (e.keyCode === 13 /* ENTER */) {
+      // create a new line with the text input and remove the prompt
+      const text = this.#$prompt.innerText;
+      this.write(text + "\n");
+      this.#$prompt.innerText = "";
+      this.#$prompt.remove();
+
+      // return the inputed text
+      this.#inputCallback(text);
+      
+      // remove the callback and the key handler
+      this.#inputCallback = undefined;
+    } else if (e.keyCode === 8 /* BACKSPACE */) {
+      this.#$prompt.innerText = this.#$prompt.innerText.slice(0, -1);
+    } else {
+      this.#$prompt.innerHtml = '';
+      this.#$prompt.innerText =  this.#$prompt.innerText + e.key;
+    }
+  }
+
+  /**
+   * Clear the terminal.
+   * Remove all lines.
+   * 
+   * @public
+   */
+  clear() {
+    this.$output.innerText = "";
+  }
+
+  /**
+   * TODO:
+   * 
+   * @public
+   * @param {*} htmlContent 
+   */
+  inserHtml(htmlContent) {
+    const $htmlNode = document.createElement("div");
+    $htmlNode.innerHTML = htmlContent;
+    this.$output.appendChild($htmlNode);
+    document.body.scrollTo(0, document.body.scrollHeight);
+  }
+
+  /**
+   * Write a text to the terminal.
+   * By default there is no linebreak at the end of a new line
+   * except the line ensd with a "\n".
+   * If the given text has multible linebreaks, multibe lines are inserted.
+   * 
+   * @public
+   * @param {string} text 
+   */
+  write(text) {
+    if (text.match(/^\n*$/)) {
+      // empty new line
+      text.match(/\n/g).forEach(() => {
+        const $br = document.createElement("br");
+        this.$output.appendChild($br);
+      });
+    } else if (text && text.length && text.includes("\n")) {
+      const lines = text.split("\n");
+      lines.forEach((line) => {
+        if (line.length === 0 || line.match(/^\s*$/)) {
+          this.$output.appendChild(document.createElement("br"));
+        } else {
+          const $lineNode = this.#newLine(line);
+          this.$output.appendChild($lineNode);
+          //this.$node.appendChild(document.createElement("br"));
+        }
+      });
+    } else if (text && text.length) {
+      // simple line
+      const $lineNode = this.#newLine(text);
+      this.$output.appendChild($lineNode);
+    }
+
+    // scroll to the buttom of the page
+    document.body.scrollTo(0, document.body.scrollHeight);
+  }
+
+  /**
+   * Like "write" but with a newline at the end.
+   * 
+   * @public
+   * @param {*} text 
+   */
+  writeln(text) {
+    this.write(text + "\n");
+  }
+
+  /**
+   * Query from user input.
+   * This is done by adding a input-element at the end of the terminal,
+   * that showes a prompt and a blinking cursor.
+   * If a key is pressed the input is added to the prompt element.
+   * The input ends with a linebreak.
+   * 
+   * @public
+   * @param {*} callback 
+   */
+  input(callback) {
+    // show prompt with a blinking prompt
+    this.$output.appendChild(this.#$prompt);
+    this.#inputCallback = callback;
+  }
+}
diff --git a/00_Common/javascript/WebTerminal/terminal.html b/00_Common/javascript/WebTerminal/terminal.html
new file mode 100644
index 00000000..626617a1
--- /dev/null
+++ b/00_Common/javascript/WebTerminal/terminal.html
@@ -0,0 +1,116 @@
+
+  
+    Minimal node.js terminal
+    
+    
+    
+    
+  
+  
+    
+

BASIC Computer Games

+
+
+ + + + diff --git a/00_Common/javascript/WebTerminal/terminal_tests.mjs b/00_Common/javascript/WebTerminal/terminal_tests.mjs new file mode 100644 index 00000000..804de3f2 --- /dev/null +++ b/00_Common/javascript/WebTerminal/terminal_tests.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +import { print, println, tab, input } from '../common.mjs'; + +async function main() { + println(tab(20), "Minimal node.js terminal 2"); + println(""); + println(tab(0), "tab 0"); + println(tab(5), "tab 5"); + println(tab(10), "tab 10"); + println(tab(15), "tab 15"); + println(tab(20), "tab 20"); + println(tab(25), "tab 25"); + println(""); + println("1234567890", " ", "ABCDEFGHIJKLMNOPRSTUVWXYZ"); + println(""); + print("\nHallo"); print(" "); print("Welt!\n"); + println(""); + print("Line 1\nLine 2\nLine 3\nLine 4"); + println(""); + + const value = await input("input"); + println(`input value was "${value}"`); + + println("End of script"); + + // 320 END + process.exit(0); +} +main(); diff --git a/00_Common/javascript/common.mjs b/00_Common/javascript/common.mjs new file mode 100644 index 00000000..3fcc4212 --- /dev/null +++ b/00_Common/javascript/common.mjs @@ -0,0 +1,21 @@ + +export function print(...messages) { + process.stdout.write(messages.join("")); +} + +export function println(...messages) { + process.stdout.write(messages.join("") + "\n"); +} + +export function tab(count) { + return " ".repeat(count); +} + +export async function input(message = "") { + process.stdout.write(message + ' '); + return new Promise(resolve => { + process.stdin.on('data', (input) => { + resolve(input.toString().replace('\n', '')); + }); + }); +} diff --git a/00_Utilities/build-index.js b/00_Utilities/build-index.js index 2785957e..597ef191 100644 --- a/00_Utilities/build-index.js +++ b/00_Utilities/build-index.js @@ -16,14 +16,23 @@ const JAVASCRIPT_FOLDER = 'javascript'; const IGNORE_FOLDERS_START_WITH = ['.', '00_', 'buildJvm', 'Sudoku']; function createGameLinks(game) { - if (game.htmlFiles.length > 1) { - const entries = game.htmlFiles.map(htmlFile => { - const name = path.basename(htmlFile).replace('.html', ''); + const creatFileLink = (file, name = path.basename(file)) => { + if (file.endsWith('.html')) { return ` -
  • - ${name} -
  • +
  • ${name.replace('.html', '')}
  • `; + } else if (file.endsWith('.mjs')) { + return ` +
  • ${name.replace('.mjs', '')} (node.js)
  • + `; + } else { + throw new Error(`Unknown file-type found: ${file}`); + } + } + + if (game.files.length > 1) { + const entries = game.files.map(file => { + return creatFileLink(file); }); return `
  • @@ -32,7 +41,7 @@ function createGameLinks(game) {
  • `; } else { - return `
  • ${game.name}
  • `; + return creatFileLink(game.files[0], game.name); } } @@ -73,11 +82,11 @@ function createIndexHtml(title, games) { `.trim().replace(/\s\s+/g, ''); } -function findHtmlFilesInFolder(folder) { +function findJSFilesInFolder(folder) { // filter folders that do not include a subfolder called "javascript" const hasJavascript = fs.existsSync(`${folder}/${JAVASCRIPT_FOLDER}`); if (!hasJavascript) { - throw new Error(`Game "${folder}" is missing a javascript implementation`); + throw new Error(`Game "${folder}" is missing a javascript folder`); } // get all files in the javascript folder @@ -85,12 +94,19 @@ function findHtmlFilesInFolder(folder) { // filter files only allow .html files const htmlFiles = files.filter(file => file.endsWith('.html')); + const mjsFiles = files.filter(file => file.endsWith('.mjs')); + const entries = [ + ...htmlFiles, + ...mjsFiles + ]; + - if (htmlFiles.length == 0) { - throw new Error(`Game "${folder}" is missing a html file in the "${folder}/${JAVASCRIPT_FOLDER}" folder`); + + if (entries.length == 0) { + throw new Error(`Game "${folder}" is missing a HTML or node.js file in the folder "${folder}/${JAVASCRIPT_FOLDER}"`); } - return htmlFiles.map(htmlFile => path.join(folder, JAVASCRIPT_FOLDER, htmlFile)); + return entries.map(file => path.join(folder, JAVASCRIPT_FOLDER, file)); } function main() { @@ -111,10 +127,10 @@ function main() { // get name and javascript file from folder const games = folders.map(folder => { const name = folder.replace('_', ' '); - let htmlFiles; + let files; try { - htmlFiles = findHtmlFilesInFolder(folder); + files = findJSFilesInFolder(folder); } catch (error) { console.warn(`Game "${name}" is missing a javascript implementation: ${error.message}`); return null; @@ -122,7 +138,7 @@ function main() { return { name, - htmlFiles + files } }).filter(game => game !== null); diff --git a/00_Utilities/javascript/style_terminal.css b/00_Utilities/javascript/style_terminal.css index 1301983f..dce94bb9 100644 --- a/00_Utilities/javascript/style_terminal.css +++ b/00_Utilities/javascript/style_terminal.css @@ -31,7 +31,11 @@ body { background-color: var(--background); color: var(--text); font: var(--font); - padding: 3rem; + margin: 0; +} + +#output { + padding: 1rem; } /* format input fields */ @@ -80,7 +84,10 @@ a:hover { } /* add all the face flicker effects (only on desktop) */ -@media screen and (min-width: 640px) { +@media screen and (min-width: 960px) { + main { + padding: 3rem; + } @keyframes flicker { 0% { opacity: 0.27861; diff --git a/74_Rock_Scissors_Paper/javascript/rockscissors.html b/74_Rock_Scissors_Paper/javascript/rockscissors.html deleted file mode 100644 index 3b5199a4..00000000 --- a/74_Rock_Scissors_Paper/javascript/rockscissors.html +++ /dev/null @@ -1,10 +0,0 @@ - - -ROCK, SCISSORS, PAPER - - - -
    
    -
    -
    -
    diff --git a/74_Rock_Scissors_Paper/javascript/rockscissors.js b/74_Rock_Scissors_Paper/javascript/rockscissors.js
    deleted file mode 100644
    index 707712fb..00000000
    --- a/74_Rock_Scissors_Paper/javascript/rockscissors.js
    +++ /dev/null
    @@ -1,107 +0,0 @@
    -// ROCK, SCISSORS, PAPER
    -//
    -// Converted from BASIC to Javascript by Oscar Toledo G. (nanochess)
    -//
    -
    -function print(str)
    -{
    -    document.getElementById("output").appendChild(document.createTextNode(str));
    -}
    -
    -function input()
    -{
    -    var input_element;
    -    var input_str;
    -
    -    return new Promise(function (resolve) {
    -                       input_element = document.createElement("INPUT");
    -
    -                       print("? ");
    -                       input_element.setAttribute("type", "text");
    -                       input_element.setAttribute("length", "50");
    -                       document.getElementById("output").appendChild(input_element);
    -                       input_element.focus();
    -                       input_str = undefined;
    -                       input_element.addEventListener("keydown", function (event) {
    -                                                      if (event.keyCode == 13) {
    -                                                      input_str = input_element.value;
    -                                                      document.getElementById("output").removeChild(input_element);
    -                                                      print(input_str);
    -                                                      print("\n");
    -                                                      resolve(input_str);
    -                                                      }
    -                                                      });
    -                       });
    -}
    -
    -function tab(space)
    -{
    -    var str = "";
    -    while (space-- > 0)
    -        str += " ";
    -    return str;
    -}
    -
    -// Main control section
    -async function main()
    -{
    -    print(tab(21) + "GAME OF ROCK, SCISSORS, PAPER\n");
    -    print(tab(15) + "CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY\n");
    -    print("\n");
    -    print("\n");
    -    print("\n");
    -    while (1) {
    -        print("HOW MANY GAMES");
    -        q = parseInt(await input());
    -        if (q >= 11)
    -            print("SORRY, BUT WE AREN'T ALLOWED TO PLAY THAT MANY.\n");
    -        else
    -            break;
    -    }
    -    h = 0;  // Human
    -    c = 0;  // Computer
    -    for (g = 1; g <= q; g++ ) {
    -        print("\n");
    -        print("GAME NUMBER " + g + "\n");
    -        x = Math.floor(Math.random() * 3 + 1);
    -        while (1) {
    -            print("3=ROCK...2=SCISSORS...1=PAPER\n");
    -            print("1...2...3...WHAT'S YOUR CHOICE");
    -            k = parseInt(await input());
    -            if (k != 1 && k != 2 && k != 3)
    -                print("INVALID.\n");
    -            else
    -                break;
    -        }
    -        print("THIS IS MY CHOICE...");
    -        switch (x) {
    -            case 1:
    -                print("...PAPER\n");
    -                break;
    -            case 2:
    -                print("...SCISSORS\n");
    -                break;
    -            case 3:
    -                print("...ROCK\n");
    -                break;
    -        }
    -        if (x == k) {
    -            print("TIE GAME.  NO WINNER.\n");
    -        } else if ((x > k && (k != 1 || x != 3)) || (x == 1 && k == 3)) {
    -            print("WOW!  I WIN!!!\n");
    -            c++;
    -        } else {
    -            print("YOU WIN!!!\n");
    -            h++;
    -        }
    -    }
    -    print("\n");
    -    print("HERE IS THE FINAL GAME SCORE:\n");
    -    print("I HAVE WON " + c + " GAME(S).\n");
    -    print("YOU HAVE WON " + h + " GAME(S).\n");
    -    print("AND " + (q - (c + h)) + " GAME(S) ENDED IN A TIE.\n");
    -    print("\n");
    -    print("THANKS FOR PLAYING!!\n");
    -}
    -
    -main();
    diff --git a/74_Rock_Scissors_Paper/javascript/rockscissors.mjs b/74_Rock_Scissors_Paper/javascript/rockscissors.mjs
    new file mode 100644
    index 00000000..be2328f2
    --- /dev/null
    +++ b/74_Rock_Scissors_Paper/javascript/rockscissors.mjs
    @@ -0,0 +1,117 @@
    +#!/usr/bin/env node
    +// ROCK, SCISSORS, PAPER
    +//
    +// Converted from BASIC to Javascript by Alexander Wunschik (mojoaxel)
    +
    +import { println, tab, input } from '../../00_Common/javascript/common.mjs';
    +
    +let userWins = 0;
    +let computerWins = 0;
    +let ties = 0;
    +
    +// 30 INPUT "HOW MANY GAMES";Q
    +// 40 IF Q<11 THEN 60
    +// 50 PRINT "SORRY, BUT WE AREN'T ALLOWED TO PLAY THAT MANY.": GOTO 30
    +// 60 FOR G=1 TO Q
    +async function getGameCount() {
    +	let gameCount = await input("HOW MANY GAMES");
    +	if (gameCount > 10) {
    +		println("SORRY, BUT WE AREN'T ALLOWED TO PLAY THAT MANY.");
    +		return await getGameCount();
    +	}
    +	return gameCount;
    +}
    +
    +// #90 PRINT "3=ROCK...2=SCISSORS...1=PAPER"
    +// #100 INPUT "1...2...3...WHAT'S YOUR CHOICE";K
    +// #110 IF (K-1)*(K-2)*(K-3)<>0 THEN PRINT "INVALID.": GOTO 90
    +async function getUserInput() {
    +	println("3=ROCK...2=SCISSORS...1=PAPER");
    +	const userChoice = await input("1...2...3...WHAT'S YOUR CHOICE");
    +	if (userChoice < 1 || userChoice > 3) {
    +		println("INVALID.");
    +		return await getUserInput();
    +	}
    +	return userChoice;
    +}
    +
    +async function game() {
    +	// 10 PRINT TAB(21);"GAME OF ROCK, SCISSORS, PAPER"
    +	// 20 PRINT TAB(15);"CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY"
    +	// 25 PRINT:PRINT:PRINT
    +	println(tab(21), 'GAME OF ROCK, SCISSORS, PAPER');
    +	println(tab(15), 'CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY');
    +	println('\n\n');
    +
    +	let gameCount = await getGameCount();
    +
    +	async function playGame(gameNumber) {
    +		// 70 PRINT: PRINT "GAME NUMBER";G
    +		println("\nGAME NUMBER ", gameNumber);
    +
    +		const ROCK = 3;
    +		const SCISSORS = 2;
    +		const PAPER = 1;
    +
    +		const usersChoice = await getUserInput();
    +
    +		// 80 X=INT(RND(1)*3+1)
    +		const computersChoice = Math.floor(Math.random()*3) + 1;
    +
    +		// 120 PRINT "THIS IS MY CHOICE..."
    +		// 130 ON X GOTO 140,150,160
    +		// 140 PRINT "...PAPER": GOTO 170
    +		// 150 PRINT "...SCISSORS": GOTO 170
    +		// 160 PRINT "...ROCK"
    +		println("THIS IS MY CHOICE...", 
    +			computersChoice === PAPER ? "...PAPER" : 
    +				computersChoice === SCISSORS ? "...SCISSORS" : 
    +					"...ROCK");
    +
    +
    +		// 170 IF X=K THEN 250
    +		// 180 IF X>K THEN 230
    +		// 190 IF X=1 THEN 210
    +		// 200 PRINT "YOU WIN!!!":H=H+1: GOTO 260
    +		// 210 IF K<>3 THEN 200
    +		// 220 PRINT "WOW!  I WIN!!!":C=C+1:GOTO 260
    +		// 230 IF K<>1 OR X<>3 THEN 220
    +		// 240 GOTO 200
    +		// 250 PRINT "TIE GAME.  NO WINNER."
    +		if (computersChoice == usersChoice) {
    +			println("TIE GAME.  NO WINNER.");
    +			ties++;
    +		} else if (
    +			(computersChoice == ROCK && usersChoice == SCISSORS) ||
    +			(computersChoice == PAPER && usersChoice == ROCK) ||
    +			(computersChoice == SCISSORS && usersChoice == PAPER)
    +		) {
    +			println("WOW!  I WIN!!!");
    +			computerWins++;
    +		} else {
    +			println("YOU WIN!!!");
    +			userWins++;
    +		}
    +	}
    +
    +	for (let gameNumber = 1; gameNumber <= gameCount; gameNumber++) {
    +		await playGame(gameNumber);
    +		// 260 NEXT G
    +	}
    +
    +	// 270 PRINT: PRINT "HERE IS THE FINAL GAME SCORE:"
    +	// 280 PRINT "I HAVE WON";C;"GAME(S)."
    +	// 290 PRINT "YOU HAVE WON";H;"GAME(S)."
    +	// 300 PRINT "AND";Q-(C+H20);"GAME(S) ENDED IN A TIE."
    +	println("\nHERE IS THE FINAL GAME SCORE:");
    +	println(`I HAVE WON ${computerWins} GAME(S).`);
    +	println(`YOU HAVE WON ${userWins} GAME(S).`);
    +	println(`AND ${ties} GAME(S) ENDED IN A TIE.`);
    +
    +	// 310 PRINT: PRINT "THANKS FOR PLAYING!!"
    +	println("\nTHANKS FOR PLAYING!!");
    +	
    +	// 320 END
    +	process.exit(0);
    +}
    +game();
    diff --git a/78_Sine_Wave/javascript/sinewave.html b/78_Sine_Wave/javascript/sinewave.html
    deleted file mode 100644
    index c9ff7d9b..00000000
    --- a/78_Sine_Wave/javascript/sinewave.html
    +++ /dev/null
    @@ -1,16 +0,0 @@
    -
    -
    -SINE WAVE
    -
    -
    -
    -
    
    -
    -
    -
    -
    diff --git a/78_Sine_Wave/javascript/sinewave.js b/78_Sine_Wave/javascript/sinewave.js
    deleted file mode 100644
    index 3823273d..00000000
    --- a/78_Sine_Wave/javascript/sinewave.js
    +++ /dev/null
    @@ -1,22 +0,0 @@
    -print(tab(30), "SINE WAVE");
    -print(tab(15), "CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY");
    -print("\n\n\n\n");
    -
    -// REMARKABLE PROGRAM BY DAVID AHL
    -// Transliterated to Javascript by Les Orchard 
    -
    -let toggleWord = true;
    -
    -for (let step = 0; step < 40; step += 0.25) {
    -  let indent = Math.floor(26 + 25 * Math.sin(step));
    -  print(tab(indent), toggleWord ? "CREATIVE" : "COMPUTING");
    -  toggleWord = !toggleWord;
    -}
    -
    -function print(...messages) {
    -  console.log(messages.join(" "));
    -}
    -
    -function tab(count) {
    -  return " ".repeat(count);
    -}
    diff --git a/78_Sine_Wave/javascript/sinewave.mjs b/78_Sine_Wave/javascript/sinewave.mjs
    new file mode 100644
    index 00000000..bd390e75
    --- /dev/null
    +++ b/78_Sine_Wave/javascript/sinewave.mjs
    @@ -0,0 +1,18 @@
    +#!/usr/bin/env node
    +
    +import { println, tab } from '../../00_Common/javascript/common.mjs';
    +
    +println(tab(30), "SINE WAVE");
    +println(tab(15), "CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY");
    +println("\n".repeat(4));
    +
    +// REMARKABLE PROGRAM BY DAVID AHL
    +// Transliterated to Javascript by Les Orchard 
    +
    +let toggleWord = true;
    +
    +for (let step = 0; step < 40; step += 0.25) {
    +  let indent = Math.floor(26 + 25 * Math.sin(step));
    +  println(tab(indent), toggleWord ? "CREATIVE" : "COMPUTING");
    +  toggleWord = !toggleWord;
    +}
    diff --git a/HOW_TO_RUN_THE_GAMES.md b/HOW_TO_RUN_THE_GAMES.md
    index f1998ddd..688ee136 100644
    --- a/HOW_TO_RUN_THE_GAMES.md
    +++ b/HOW_TO_RUN_THE_GAMES.md
    @@ -40,9 +40,21 @@ or if you are **using JDK11 or later** you can now execute a self contained java
     
     ## javascript
     
    -The javascript examples can be run from within your web browser:
    +There are two ways of javascript implementations:
     
    -1. Simply open the corresponding `.html` file from your web browser.
    +### browser
    +
    +The html examples can be run from within your web browser. Simply open the corresponding `.html` file from your web browser.
    +
    +### node.js
    +
    +Some games are implemented as a [node.js](https://nodejs.org/) script. In this case there is no `*.html` file in the folder.
    +
    +1. [install node.js](https://nodejs.org/en/download/) for your system.
    +1. change directory to the root of this repository (e.g. `cd basic-computer-games`).
    +1. from a terminal call the script you want to run (e.g. `node 78_Sine_Wave/javascript/sinewave.mjs`).
    +
    +_Hint: Normally javascript files have a `*.js` extension. We are using `*.mjs` to let node know , that we are using [ES modules](https://nodejs.org/docs/latest/api/esm.html#modules-ecmascript-modules) instead of [CommonJS](https://nodejs.org/docs/latest/api/modules.html#modules-commonjs-modules)._
     
     ## kotlin
     
    diff --git a/index.html b/index.html
    index 45e6ec84..9b38c627 100644
    --- a/index.html
    +++ b/index.html
    @@ -1 +1 @@
    -BASIC Computer Games

    BASIC Computer Games

    \ No newline at end of file +BASIC Computer Games

    BASIC Computer Games

    \ No newline at end of file