mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-31 15:07:42 -08:00
Merge pull request #687 from coding-horror/74-html-terminal
javascript node.js scripts
This commit is contained in:
72
00_Common/javascript/WebTerminal/HtmlTerminal.css
Normal file
72
00_Common/javascript/WebTerminal/HtmlTerminal.css
Normal file
@@ -0,0 +1,72 @@
|
||||
:root {
|
||||
--terminal-font: 1rem "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 {
|
||||
display: block;
|
||||
font: var(--terminal-font);
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
max-width: 60rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
195
00_Common/javascript/WebTerminal/HtmlTerminal.js
Normal file
195
00_Common/javascript/WebTerminal/HtmlTerminal.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* @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 if (
|
||||
e.keyCode == 16 // "Shift"
|
||||
|| e.keyCode == 17 // "Control"
|
||||
|| e.keyCode == 20 // "CapsLock"
|
||||
|| !e.key.match(/^[a-z0-9!"§#$%&'()*+,.\/:;<=>?@\[\] ^_`{|}~-]$/i)
|
||||
) {
|
||||
// ignore non-visible characters
|
||||
return e;
|
||||
} else {
|
||||
this.#$prompt.innerHtml = '';
|
||||
const key = e.shiftKey ? e.key.toUpperCase() : e.key;
|
||||
this.#$prompt.innerText = this.#$prompt.innerText + 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 || text.length <= 0) {
|
||||
// empty line
|
||||
this.$output.appendChild(document.createElement("br"));
|
||||
} else if (text.endsWith("\n")) {
|
||||
// single line with linebrank
|
||||
const $lineNode = this.#newLine(text);
|
||||
this.$output.appendChild(this.#newLine(text));
|
||||
this.$output.appendChild(document.createElement("br"));
|
||||
} else if (text.includes("\n")) {
|
||||
// multible lines
|
||||
const lines = text.split("\n");
|
||||
lines.forEach((line) => {
|
||||
this.write(line);
|
||||
});
|
||||
} else {
|
||||
// single line
|
||||
this.$output.appendChild(this.#newLine(text));
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
129
00_Common/javascript/WebTerminal/terminal.html
Normal file
129
00_Common/javascript/WebTerminal/terminal.html
Normal file
@@ -0,0 +1,129 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Minimal node.js terminal</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=.75">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="../../../00_Utilities/javascript/style_terminal.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="HtmlTerminal.css" />
|
||||
<style>
|
||||
header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-bottom: 1px solid var(--text);
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0;
|
||||
margin-bottom: 1rem;
|
||||
background: black;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
header h1 {
|
||||
font-size: small;
|
||||
color: var(--text),
|
||||
}
|
||||
header div {
|
||||
font-size: small;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="../../../">BASIC Computer Games</a></h1>
|
||||
</header>
|
||||
<main id="output"></main>
|
||||
<script src="HtmlTerminal.js" type="text/javascript"></script>
|
||||
<script>
|
||||
const $output = document.getElementById("output");
|
||||
const term = new HtmlTerminal($output);
|
||||
|
||||
function getGameScriptFromHash() {
|
||||
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("#", "");
|
||||
return gameFile;
|
||||
}
|
||||
|
||||
function addGitHubLink(gameFile) {
|
||||
const gameFolder = gameFile.split("/")[0];
|
||||
|
||||
$gitHubLink = document.createElement("a");
|
||||
$gitHubLink.href = `https://github.com/coding-horror/basic-computer-games/tree/main/${gameFolder}`;
|
||||
$gitHubLink.innerText = `show source-code`;
|
||||
|
||||
var $gitHubBanner = document.createElement("div");
|
||||
$gitHubBanner.classList.add("githublink");
|
||||
$gitHubBanner.appendChild($gitHubLink);
|
||||
|
||||
const $header = document.getElementsByTagName('header')[0];
|
||||
$header.append($gitHubBanner);
|
||||
}
|
||||
|
||||
function loadGameScript(gameFile) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine how much chars will fit in each terminal line.
|
||||
*/
|
||||
function getOutputColumns($element) {
|
||||
|
||||
const fontWidth = 10; //TODO: this width could be measured but it may be complicated!
|
||||
const columnWidth = Math.trunc($element.clientWidth / fontWidth);
|
||||
console.warn(`[terminal] document.body.clientWidth:${$element.clientWidth} fontsize:${fontWidth} columnWidth:${columnWidth}`);
|
||||
return columnWidth;
|
||||
}
|
||||
|
||||
/* 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),
|
||||
columns: getOutputColumns($output)
|
||||
},
|
||||
stdin: {
|
||||
on: (event, callback) => term.input(callback),
|
||||
},
|
||||
exit: (code) => {},
|
||||
};
|
||||
|
||||
// let's play 🚀
|
||||
const gameFile = getGameScriptFromHash();
|
||||
addGitHubLink(gameFile);
|
||||
loadGameScript(gameFile);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
30
00_Common/javascript/WebTerminal/terminal_tests.mjs
Normal file
30
00_Common/javascript/WebTerminal/terminal_tests.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { print, println, tab, input } from '../common.mjs';
|
||||
|
||||
async function main() {
|
||||
println(tab(30), "Minimal node.js terminal emulator");
|
||||
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("");
|
||||
println("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();
|
||||
67
00_Common/javascript/common.mjs
Normal file
67
00_Common/javascript/common.mjs
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Print multible strings to the terminal.
|
||||
* Strings get concatinated (add together) without any space betweent them.
|
||||
* There will be no newline at the end!
|
||||
* If you want a linebrak at the end use `println`.
|
||||
*
|
||||
* This function is normally used if you want to put something on the screen
|
||||
* and later add some content to the same line.
|
||||
* For normal output (similar to `console.log`) use `println`!
|
||||
*
|
||||
* @param {...string} messages - the strings to print to the terminal.
|
||||
*/
|
||||
export function print(...messages) {
|
||||
process.stdout.write(messages.join(""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multible strings as a new line to the terminal.
|
||||
* Strings get concatinated (add together) without any space betweent them.
|
||||
* There will be a newline at the end!
|
||||
* If you want the terminal to stay active on the current line use `print`.
|
||||
*
|
||||
* @param {...any} messages - the strings to print to the terminal.
|
||||
*/
|
||||
export function println(...messages) {
|
||||
process.stdout.write(messages.join("") + "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty string with a given length
|
||||
*
|
||||
* @param {number} length - the length of the string in space-characters.
|
||||
* @returns {string} returns a string containing only ampty spaces with a length of `count`.
|
||||
*/
|
||||
export function tab(length) {
|
||||
return " ".repeat(length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read input from the keyboard and return it as a string.
|
||||
* TODO: to would be very helpfull to only allow a certain class of input (numbers, letters)
|
||||
* TODO: also we could convert all inputs to uppercase (where it makes sence).
|
||||
*
|
||||
* @param {string=''} message - a message or question to print befor the input.
|
||||
* @returns {Promise<string>} - returns the entered text as a string
|
||||
* @async
|
||||
*/
|
||||
export async function input(message = '') {
|
||||
/* First we need to print the mesage
|
||||
* We append a space by default to seperate the message from the imput.
|
||||
* TODO: If the message already contains a space at the end this is not needed! */
|
||||
process.stdout.write(message + ' ');
|
||||
|
||||
return new Promise(resolve => {
|
||||
process.stdin.on('data', (input) => {
|
||||
/* onData returns a Buffer.
|
||||
* First we need to convert it into a string. */
|
||||
const data = input.toString();
|
||||
|
||||
/* The result fo onData is a string ending with an `\n`.
|
||||
* We just need the actual content so let's remove the newline at the end: */
|
||||
const content = data[data.length] === '\n' ? data.slice(0, -1) : data;
|
||||
|
||||
resolve(content);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -14,16 +14,29 @@ const path = require('path');
|
||||
const TITLE = 'BASIC Computer Games';
|
||||
const JAVASCRIPT_FOLDER = 'javascript';
|
||||
const IGNORE_FOLDERS_START_WITH = ['.', '00_', 'buildJvm', 'Sudoku'];
|
||||
const IGNORE_FILES = [
|
||||
// "84 Super Star Trek" has it's own node/js implementation (using xterm)
|
||||
'cli.mjs', 'superstartrek.mjs'
|
||||
];
|
||||
|
||||
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 `
|
||||
<li>
|
||||
<a href="${htmlFile}">${name}</a>
|
||||
</li>
|
||||
<li><a href="${file}">${name.replace('.html', '')}</a></li>
|
||||
`;
|
||||
} else if (file.endsWith('.mjs')) {
|
||||
return `
|
||||
<li><a href="./00_Common/javascript/WebTerminal/terminal.html#${file}">${name.replace('.mjs', '')} (node.js)</a></li>
|
||||
`;
|
||||
} else {
|
||||
throw new Error(`Unknown file-type found: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (game.files.length > 1) {
|
||||
const entries = game.files.map(file => {
|
||||
return creatFileLink(file);
|
||||
});
|
||||
return `
|
||||
<li>
|
||||
@@ -32,7 +45,7 @@ function createGameLinks(game) {
|
||||
</li>
|
||||
`;
|
||||
} else {
|
||||
return `<li><a href="${game.htmlFiles}">${game.name}</a></li>`;
|
||||
return creatFileLink(game.files[0], game.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,11 +86,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 +98,17 @@ 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
|
||||
].filter(file => !IGNORE_FILES.includes(file));
|
||||
|
||||
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 +129,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 +140,7 @@ function main() {
|
||||
|
||||
return {
|
||||
name,
|
||||
htmlFiles
|
||||
files
|
||||
}
|
||||
}).filter(game => game !== null);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,37 +1,23 @@
|
||||
import * as readline from 'readline'
|
||||
#!/usr/bin/env node
|
||||
|
||||
// start reusable code
|
||||
async function input(prompt = "") {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
import { println, input } from '../../00_Common/javascript/common.mjs';
|
||||
|
||||
return new Promise((resolve, _) => {
|
||||
rl.setPrompt(prompt)
|
||||
// show user the question
|
||||
rl.prompt()
|
||||
// listen for user answer,
|
||||
// callback is triggered as soon as user hits enter key
|
||||
rl.on('line', answer => {
|
||||
rl.close()
|
||||
// resolve the promise, with the input the user entered
|
||||
resolve(answer)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function println(message = "", align = "left"){
|
||||
let padColCount = 0
|
||||
if(align === "center"){
|
||||
function printAlign(message = "", align = "left") {
|
||||
// process.stdout.columns is the number of spaces per line in the terminal
|
||||
const maxWidth = process.stdout.columns
|
||||
if (align === "center") {
|
||||
// calculate the amount of spaces required to center the message
|
||||
// process.stdout.columns is the number of spaces per line in the terminal
|
||||
padColCount = Math.round(process.stdout.columns / 2 + message.length / 2)
|
||||
const padColCount = Math.round((process.stdout.columns-message.length)/2);
|
||||
const padding = padColCount <= 0 ? '' : ' '.repeat(padColCount);
|
||||
println(padding, message);
|
||||
} else if (align === "right") {
|
||||
const padColCount = Math.round(process.stdout.columns-message.length);
|
||||
const padding = padColCount <= 0 ? '' : ' '.repeat(padColCount);
|
||||
println(padding, message);
|
||||
} else {
|
||||
println(message);
|
||||
}
|
||||
console.log(message.padStart(padColCount, " "))
|
||||
}
|
||||
// end reusable code
|
||||
|
||||
|
||||
function equalIgnoreCase(correct, provided){
|
||||
return correct.toString().toLowerCase() === provided.toString().toLowerCase()
|
||||
@@ -39,8 +25,10 @@ function equalIgnoreCase(correct, provided){
|
||||
|
||||
async function evaluateQuestion(question, answerOptions, correctAnswer, correctMessage, wrongMessage){
|
||||
// ask the user to answer the given question
|
||||
println(question);
|
||||
println(answerOptions.map((answer, index) => `${index+1})${answer}`).join(', '));
|
||||
// this is a blocking wait
|
||||
const answer = await input(question + "\n" + answerOptions + "\n")
|
||||
const answer = await input('?')
|
||||
const isCorrect = equalIgnoreCase(correctAnswer, answer)
|
||||
println(isCorrect ? correctMessage : wrongMessage)
|
||||
return isCorrect ? 1 : 0
|
||||
@@ -48,31 +36,40 @@ async function evaluateQuestion(question, answerOptions, correctAnswer, correctM
|
||||
|
||||
async function main(){
|
||||
let score = 0
|
||||
println("LITERATURE QUIZ", "center")
|
||||
println("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY", "center")
|
||||
println();println();println()
|
||||
|
||||
printAlign("LITERATURE QUIZ", "center")
|
||||
printAlign("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY", "center")
|
||||
println("\n\n")
|
||||
|
||||
println("TEST YOUR KNOWLEDGE OF CHILDREN'S LITERATURE.");
|
||||
println();
|
||||
println("THIS IS A MULTIPLE-CHOICE QUIZ.");
|
||||
println("TYPE A 1, 2, 3, OR 4 AFTER THE QUESTION MARK.");
|
||||
println();
|
||||
println("GOOD LUCK!");
|
||||
println("\n\n");
|
||||
|
||||
score += await evaluateQuestion("IN PINOCCHIO, WHAT WAS THE NAME OF THE CAT?",
|
||||
"1)TIGGER, 2)CICERO, 3)FIGARO, 4)GUIPETTO", 3,
|
||||
[ "TIGGER", "CICERO", "FIGARO", "GUIPETTO"], 3,
|
||||
"VERY GOOD! HERE'S ANOTHER.", "SORRY...FIGARO WAS HIS NAME.")
|
||||
println()
|
||||
|
||||
score += await evaluateQuestion("FROM WHOSE GARDEN DID BUGS BUNNY STEAL THE CARROTS?",
|
||||
"1)MR. NIXON'S, 2)ELMER FUDD'S, 3)CLEM JUDD'S, 4)STROMBOLI'S", 2,
|
||||
[ "MR. NIXON'S", "ELMER FUDD'S", "CLEM JUDD'S", "STROMBOLI'S" ], 2,
|
||||
"PRETTY GOOD!", "TOO BAD...IT WAS ELMER FUDD'S GARDEN.")
|
||||
println()
|
||||
|
||||
score += await evaluateQuestion("IN THE WIZARD OF OS, DOROTHY'S DOG WAS NAMED",
|
||||
"1)CICERO, 2)TRIXIA, 3)KING, 4)TOTO", 4,
|
||||
[ "CICERO", "TRIXIA", "KING", "TOTO" ], 4,
|
||||
"YEA! YOU'RE A REAL LITERATURE GIANT.",
|
||||
"BACK TO THE BOOKS,...TOTO WAS HIS NAME.")
|
||||
println()
|
||||
|
||||
score += await evaluateQuestion("WHO WAS THE FAIR MAIDEN WHO ATE THE POISON APPLE",
|
||||
"1)SLEEPING BEAUTY, 2)CINDERELLA, 3)SNOW WHITE, 4)WENDY", 3,
|
||||
[ "SLEEPING BEAUTY", "CINDERELLA", "SNOW WHITE", "WENDY" ], 3,
|
||||
"GOOD MEMORY!", "OH, COME ON NOW...IT WAS SNOW WHITE.")
|
||||
|
||||
println();println()
|
||||
println("\n")
|
||||
|
||||
if(score === 4) {
|
||||
println("WOW! THAT'S SUPER! YOU REALLY KNOW YOUR NURSERY\n"+
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
117
74_Rock_Scissors_Paper/javascript/rockscissors.mjs
Normal file
117
74_Rock_Scissors_Paper/javascript/rockscissors.mjs
Normal 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();
|
||||
@@ -1,16 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>SINE WAVE</title>
|
||||
<link rel="stylesheet" href="../../00_Utilities/javascript/style_terminal.css" />
|
||||
</head>
|
||||
<body>
|
||||
<pre id="output"></pre>
|
||||
<script>
|
||||
/* redirect console.log messages to the output-element in the DOM */
|
||||
window.console = {
|
||||
log: (text) => document.getElementById("output").innerHTML += text + "<br>"
|
||||
}
|
||||
</script>
|
||||
<script src="sinewave.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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);
|
||||
}
|
||||
18
78_Sine_Wave/javascript/sinewave.mjs
Normal file
18
78_Sine_Wave/javascript/sinewave.mjs
Normal 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;
|
||||
}
|
||||
@@ -2,12 +2,9 @@ import {
|
||||
onExit,
|
||||
onPrint,
|
||||
onInput,
|
||||
setGameOptions,
|
||||
getGameState,
|
||||
gameMain,
|
||||
} from "./superstartrek.mjs";
|
||||
|
||||
import util from "util";
|
||||
import readline from "readline";
|
||||
|
||||
onExit(function exit() {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>WORD</title>
|
||||
<link rel="stylesheet" href="../../00_Utilities/javascript/style_terminal.css" />
|
||||
</head>
|
||||
<body>
|
||||
<pre id="output"></pre>
|
||||
<script src="word.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,43 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
// WORD
|
||||
//
|
||||
// Converted from BASIC to Javascript by Oscar Toledo G. (nanochess)
|
||||
//
|
||||
|
||||
function print(str)
|
||||
{
|
||||
document.getElementById("output").appendChild(document.createTextNode(str));
|
||||
}
|
||||
|
||||
function input()
|
||||
{
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
const 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_element.addEventListener("keydown", function (event) {
|
||||
if (event.keyCode === 13) {
|
||||
const input_str = input_element.value;
|
||||
document.getElementById("output").removeChild(input_element);
|
||||
print(input_str);
|
||||
print("\n");
|
||||
resolve(input_str);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function tab(space)
|
||||
{
|
||||
let str = "";
|
||||
while (space-- > 0)
|
||||
str += " ";
|
||||
return str;
|
||||
}
|
||||
import { print, tab, input } from '../../00_Common/javascript/common.mjs';
|
||||
|
||||
// These are the words that the game knows about> If you want a bigger challenge you could add more words to the array
|
||||
const WORDS = ["DINKY", "SMOKE", "WATER", "GLASS", "TRAIN",
|
||||
@@ -74,7 +40,7 @@ async function main()
|
||||
|
||||
let guess = undefined;
|
||||
while (1) {
|
||||
print("GUESS A FIVE LETTER WORD");
|
||||
print("GUESS A FIVE LETTER WORD:");
|
||||
guess = (await input()).toUpperCase();
|
||||
guessCount++;
|
||||
if (secretWord === guess) {
|
||||
@@ -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
Reference in New Issue
Block a user