javascript: target node.js instead of the browser

implemented minimal html terminal emulator
This commit is contained in:
Alexander Wunschik
2022-03-19 16:48:30 +01:00
parent b0ebcc7e18
commit 99ae40fb46
13 changed files with 539 additions and 144 deletions

View File

@@ -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 <br> 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 <span>
* 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);
}

View File

@@ -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 <https://github.com/mojoaxel>
*/
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 <pre class="line"></pre>
*/
#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;
}
}

View File

@@ -0,0 +1,73 @@
<html>
<head>
<title>Minimal node.js terminal</title>
<link
rel="stylesheet"
href="../../../00_Utilities/javascript/style_terminal.css"
/>
<link
rel="stylesheet"
href="HtmlTerminal.css"
/>
</head>
<body>
<div id="output"></div>
<script src="HtmlTerminal.js" type="text/javascript"></script>
<script>
const term = new HtmlTerminal(document.getElementById("output"));
function loadGameScript() {
const hash = window.location.hash;
// if no game-script was provided redirect to the overview.
if (!hash) {
// show error message and link back to the index.html
console.debug('[HtmlTerminal] No game script found!');
term.writeln(`no game script found :-(\n`);
term.inserHtml(`<a href="/">>> Back to game overview!</a>`);
return;
}
// remove the hash
const gameFile = hash.replace('#','');
// clear terminal
term.clear();
// load game-script
console.debug('[HtmlTerminal] Game script found: ', gameFile);
const gameScript = `../../../${gameFile}`;
var $scriptTag = document.createElement("script");
$scriptTag.async = 'async'
$scriptTag.type = "module";
$scriptTag.src = gameScript;
$scriptTag.onerror = () => {
term.clear();
term.writeln(`Error loading game-script "${gameFile}" :-(\n`);
term.inserHtml(`<a href="/">>> Back to game overview!</a>`);
};
$scriptTag.addEventListener('load', function() {
console.log('[HtmlTerminal] Game script loaded!');
});
document.body.append($scriptTag);
}
/* Redirect stdin/stdout to the HtmlTerminal.
* This is VERY hacky and should never be done in a serious project!
* We can use this here because we know what we are doing and...
* ...it's just simple games ;-) */
window.process = {
stdout: {
write: (t) => term.write(t),
},
stdin: {
on: (event, callback) => term.input(callback)
},
exit: (code) => {},
};
// let the games begin 🚀
loadGameScript();
</script>
</body>
</html>

View File

@@ -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();

View File

@@ -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', ''));
});
});
}

View File

@@ -31,7 +31,8 @@ body {
background-color: var(--background);
color: var(--text);
font: var(--font);
padding: 3rem;
margin: 0;
padding: 1rem;
}
/* format input fields */
@@ -80,7 +81,10 @@ a:hover {
}
/* add all the face flicker effects (only on desktop) */
@media screen and (min-width: 640px) {
@media screen and (min-width: 960px) {
body {
padding: 3rem;
}
@keyframes flicker {
0% {
opacity: 0.27861;

View File

@@ -1,10 +0,0 @@
<html>
<head>
<title>ROCK, SCISSORS, PAPER</title>
<link rel="stylesheet" href="../../00_Utilities/javascript/style_terminal.css" />
</head>
<body>
<pre id="output"></pre>
<script src="rockscissors.js"></script>
</body>
</html>

View File

@@ -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();

View File

@@ -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();

View File

@@ -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 <me@lmorchard.com>
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);
}

View File

@@ -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 <me@lmorchard.com>
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;
}

View File

@@ -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

File diff suppressed because one or more lines are too long