User Tools

Site Tools


javascript_terminal_v2

JavaScript Terminal v2

About

This is the Season 2, version 2 of JavaScript Terminal. It is more advanced than the v1 and is intended as a platform for larger and more advanced projects than simple one-scene games.

Progression

Students should move to the v2 after three to five games are made using the original terminal.

Main Differences

The primary difference is that the engine has been split into two distinct halves; 'immediate' mode, and 'run' mode.

By setting gameState = 'terminal', the program solely acts as a terminal emulator. This is known as 'immediate mode' because when enter is pressed, this sends the current line to a command processor, which takes immediate action on that line. So, if the user types a command like 'HELP' the command is processed immediately and a message is dispayed on the screen.

The next mode to understand is 'run mode', or 'command' mode. In this mode, events are pulled from a queue and handled by the event processor.

Simulating String Input

Currently, immediate mode sends unknown inputs to the run mode as events. The practical application of this is simulating string input. This will be discussed in the code review in the input() section.

When in immediate (terminal) mode, the user can control the cursor with the arrow keys. In command (run) mode, click and touch actions will generate events which can be handled by the engine if desired.

This separation of terminal mode and command move is central to the goal of escaping the limitations of JavaScript. We do this by creating the nucleus of a state machine and atomizing the execution of instructions inside the context of that machine. This is the 'VM idea'.

the input() connundrum

After I published technical demo 2 (this) I added an input() function, which led to a realization that there was no need to use state to operate the game in two modes; what was really required were a series of flags such as 'echo' (echo on, echo off), and hooks where a game could read those keystrokes. In fact, this contributes towards the 'VM idea' (see below).

I did retroactively add input() to technical demo 2 (see commands HELP and INPUT). String input in the form of an input() function works by giving the system the KEY which will identify the event-command, and a prompt string. The prompt string can be empty and no prompt string will be shown; Calling this loses program context so processing will effectively end after the input command is given. This works as follows; the user enters a 'change name' command. The program then calls

    input("SET_NAME", "What is your name? ");
    return;

Then, when the user presses ENTER, an event will be added to the queue which looks like this:

    SET_NAME Richard

Then, the game engine will handle this event by changing the user's name. In theory, the game engine will set the name before the next user event is called, either by having a priority queue for state changes, or by assumption (i.e. fiat) that there is no command in the queue which relies on SET_NAME. If so, the system could be programmed to HALT until SET_NAME executes (SET_NAME would be added and the system would wait until it changed to SET NAME <value>).

What if multiple contexts are required; such as, asking three questions in a row? Then, multiple events could be added to the queue:

    INPUT SET_NAME What is your name?
    INPUT SET_CLASS "What is your class (Fighter, Mage)?
    INPUT SET_RACE "What is your race (Human, Elf, Hobbit)?

The engine can then throw an INPUT command, and since event processing stops during INPUT, the next question will be asked once the previous one is answered.

What if you need to control state? then you can add queue commands such as:

    SET_NAME
    VAR NAME VALUE

or if you want to be fancy you can index the value by string name; ex. variables[s] where s is the string value of something like

    INPUT using A
    VAR NAME is A

The interpreter can read and write, even to a map (dictionary) if string indexing is not available.

The VM idea

The idea was that we would get around the limitations of JavaScript (or any modern game engine, or, single threaded environment) by creating a virtual machine that accepts commands, like a computer or cpu that pulls instructions and executes them in the context of some environment. This is what the original idea was, we would create an event after we processed the input in 'input mode' (such as SET_NAME). But then, it became obvious that the same system, taken a little further could also do variables, and even simulate BASIC (line numbers could be array indexes, for easy sorting and renumbering).

This led to the realization that having two modes wasn't really necessary. There should only be one mode, and when input is needed an input flag could occur which would put characters into a string buffer OR which would trigger a line read from a cursor position, or, on the current line, or context, as contextually implied by the environment. For example a command line has different parameters than a 'What is your name?' prompt in Nethack, or a simulated getkey.

Eventually it became obvious that the system was falling towards a CPU/System simulator. My mind jumped to 'emulator', even though that would be overkill, as a way to try and define an upper bound to what I would need to do.

This topic became too large to fit inside the JavaScript Terminal or JavaScript Netwhack idea, so I will write about it somewhere else, maybe JavaScript Virtual Machine.

Code Commentary

main.js

// Set up Canvas and get ctx (context) for drawing.
let canvas = document.getElementById('fsc');
let ctx = canvas.getContext('2d');

// Set up Terminal
maxtrows = 10
maxtcols = 10
terminal = new Terminal(10, 10)

// load font
fontLoader = new FontLoader('myvga', 'PxPlus_IBM_VGA_9x16.ttf');

// Put up a cute message.
function system_message() {
    terminal.clearline(0);
    terminal.clearline(1);
    terminal.clearline(2);
    terminal.clearline(3);
    terminal.clearline(4);
    terminal.clearline(5);

    msgline1 = "**** JavaScript Terminal Technical Demo v2.0 ****";
    msgline2 = maxTcols + "x" + maxTrows + " " + ctx.font;
    msgline3 = 'READY.';
    terminal.cx = Math.floor((terminal.cols - msgline1.length) / 2);
    terminal.cy = 0;
    terminal.puts(msgline1);

    terminal.cx = Math.floor((terminal.cols - msgline2.length) / 2);
    terminal.cy = 1;
    terminal.puts(msgline2);

    terminal.cx = 0;
    terminal.cy = 3;
    terminal.puts(msgline3);

    terminal.cx = 0;
    terminal.cy = 4;
}

// Function to resize and recreate the canvas
function resizeCanvas() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    maxTcols = Math.floor(canvas.width / terminal.charWidth)
    maxTrows = Math.floor(canvas.height / terminal.charHeight);

    t = new Terminal(maxTcols, maxTrows);
    terminal.copyContentTo(t);
    terminal = t;

    ctx.font = '32px myvga'; // this seems to be needed here.

    //console.log("Terminal Resize Event")
    //console.log("Available Pixel Dimensions: " + canvas.width + "x" + canvas.height);
    //console.log("Calculating Maximum Terminal Size: " + maxTcols + "x" + maxTrows);
    //console.log("Actual Pixel Dimensions: " + maxTcols * terminal.charWidth + "x" + maxTrows * terminal.charHeight);

    system_message();
}

// Resize and recreate the canvas
window.addEventListener('resize', resizeCanvas);


// Attach the click event listener to the canvas
canvas.addEventListener('click', function (event) {
    // do not process click during terminal mode.
    if (gameState === 'terminal') {
        return;
    }
    // Get the mouse coordinates relative to the canvas
    var x = event.clientX - canvas.getBoundingClientRect().left;
    var y = event.clientY - canvas.getBoundingClientRect().top;


    // Calculate Canvas Areas
    // 1. Left Side
    leftx = canvas.width * 0.363380227632; // * (1 - (2/pi)) makes the center a bit smaller than /3.
    rightx = canvas.width - leftx;
    topy = canvas.height * 0.363380227632;
    bottomy = canvas.height - topy;

    // Check if the click is within a specific region (e.g., a rectangle)
    if (x <= leftx) {
        if (y <= topy) {
            //console.log("top left")
            terminal.arrowleft();
            terminal.arrowup();
        }
        if (y >= bottomy) {
            //console.log("bottom left");
            terminal.arrowleft();
            terminal.arrowdown();
        }
        if ((y < bottomy) && (y > topy)) {
            //console.log("middle left");
            terminal.arrowleft();
        }
    }

    if (x >= rightx) {
        if (y <= topy) {
            //  console.log("top right")
            terminal.arrowright();
            terminal.arrowup();
        }
        if (y >= bottomy) {
            //console.log("bottom right");
            terminal.arrowright();
            terminal.arrowdown();
        }
        if ((y < bottomy) && (y > topy)) {
            //console.log("middle right");
            terminal.arrowright();
        }
    }

    if ((x > leftx) && (x < rightx)) {
        if (y <= topy) {
            //console.log("top middle")
            terminal.arrowup();
        }
        if (y >= bottomy) {
            //console.log("bottom middle");
            terminal.arrowdown();
        }
        if ((y < bottomy) && (y > topy)) {
            //console.log("middle middle");
            let mx = getRandomInt(0, terminal.cols - 1);
            let my = getRandomInt(0, terminal.rows - 1);
            let mch = getRandomLetter();
            let mcolor = Color.getColor(getRandomInt(0, 15));
            let mbackground = Color.black;
            terminal.setch(mx, my, mch, mcolor, mbackground);
        }
    }

});

// Keyboard listener
gameState = 'terminal'
document.addEventListener('keydown', function (event) {
    switch (gameState) {
        case "terminal":
            // log any unprintable characters.
            if (event.key.length > 1) {
                console.log(event.key)
            }
            // Put characters on terminal.
            if (event.key === 'Escape') {
                console.log("Switching to command queue mode.");
                gameState = 'command';
            } else {
                terminal.type(event.key);
            }
            break;
        case "command":
            // Enter keystrokes as console commands into the command queue.
            event_queue.push("KEY " + event.key);
            break;
        default:
            console.log("unknown game mode for key input");
            break;
    }

});

var event_queue = [];

function check_events() {
    // While there are events to process,
    while (event_queue.length > 0) {
        let cmd = event_queue.shift().trim();
        let para = '';

        if (cmd.indexOf(' ') >= 0) {
            // contains internal spaces, determine parameters.
            para = cmd.substring(cmd.indexOf(' ') + 1);
            cmd = cmd.substring(0, cmd.indexOf(' '));
        }

        cmd = cmd.toUpperCase().trim();

        if (cmd.length == 0) {
            return;
        }

        // Now, cmd is the event/command and para contains any parameters to it.
        switch (cmd) {
            case 'KEY':
                console.log("fount KEY event: [" + para + "]");
                if (para === 'Escape') {
                    console.log("Switching to terminal mode.");
                    gameState = 'terminal'
                }
                break;
            default:
                console.log("found unknown event: " + cmd + " " + para);
                break;
        } // switch
    } // while has events
} // check_events

// Animation logic, timed events, etc.
// I don't plan for this to be used much in a roguelike.
// but it is useful for (ex.) a static demo of the display engine.
function update() {
    switch (gameState) {
        case "terminal":
            // Characters are sent to a terminal function and dumped to screen buffer.
            // We don't do any special processing here.
            break;
        case "command":
            // Command mode. Process events on the event stack.
            check_events();
            break;
        default:
            // do nothing
            break;
    }

    var demo = false;
    if (demo) {
        n = getRandomInt(1, 100);
        if (n === 1) {
            x = getRandomInt(0, terminal.cols - 1)
            y = getRandomInt(0, terminal.rows - 1)
            ch = getRandomLetter()
            color = Color.getColor(getRandomInt(0, 15))
            background = Color.black
            terminal.setch(x, y, ch, color, background)
            //console.log("[" + ch + "], " + x + "," + y + " " + color);
        }
    }
}

// All we really need to do is draw the terminal.
// What is on the terminal is done using (ex.) Terminal.setch()
function render() {
    // clear screen
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // draw characters on terminal
    terminal.draw(ctx);
}

let lastTime = 0;

function gameLoop(timestamp) {
    // Update game logic here
    update()

    // Render the game state
    render();

    // Request the next frame
    requestAnimationFrame(gameLoop);
}

// Initial canvas setup
resizeCanvas();

// Test procedure
//test_screen(terminal);

terminal.cc = true;

// Wait for the font to load before starting the update/render loop.
let waitTimerId; // Variable to store the interval identifier
let loading_div;

function waitForFonts() {

    waitTimerId = setInterval(() => {
        // Check the condition (replace this with your actual condition)
        if (fontLoader.loaded) {
            // Clear the interval
            clearInterval(waitTimerId);

            // Start Game Loop
            gameLoop();
        }
    }, 100); // Set the interval time in milliseconds (e.g., 1000 ms = 1 second)
}

waitForFonts()

One major addition is the concept of the FontLoader class, which is intended to clean up main.js. With this in mind, 'waitForFonts()' should probably be moved to that class – maybe in a v3.

var gameState

The initial move is to create gameState, which will hold either 'terminal' or 'command', for the two different modes of operation. We will discuss the modes of operation in terms of practical analysis of update() and the keydown listener.

function update()

update() switches on var gameState. If gameState is 'terminal' then update does nothing. If gameState is 'command', the check_events() function will cycle through a list of events, in the same manner that a cpu executes an instruction cycle, until there are no more events (or a timer pauses the process until next frame).

keydown handler

In the same way that update() switches, the keydown handler also switches on gameState. If gameState is 'terminal', all event.key values are sent to terminal.type() which is the function that processes them as if they were typed on a terminal. Some special processing occurrs here for arrows, enter, and so forth – this could (and probably should) be moved to the terminal class to keep main.js concise.

function check_events()

check_events() acts to perform instruction cycling like how a cpu will load an instruction from memory and attempt to execute it. check_events() takes the first event in the event_queue array and switches on it to handle that event. In command mode, the keyboard handler creates events called KEY events, so that the check_events() function can deal with them as game commands. In terminal mode, the terminal will add events to this queue when the user presses enter.

So, in terminal mode, if the user types 'help', it will be processed by the terminal as a special system command. This is kind of like an immediate mode which simulates string input, firing on the ENTER key. However, if the user types anything else (such as 10 print “hello”) it is created as an event and processed when terminal mode is exited. This event would show up as 'ENTER 10 print “hello”'.

However, in command mode, any key pressed would cause the keyboard handler to create a KEY event such as “KEY A” which in many games is interpreted as 'move left' or 'strafe left'.

When check_events() sees any of these events it tries to do whatever it takes to 'handle' or 'process' the events. What it should do is just call a handler function, but this can be refactored in later.

Handling Click/Touch events

There is also a click (or, touch) handler, as an example framework for future projects. It is off in terminal mode, but in command (play) mode, pressing on the sides of the screen will move the cursor. Theoretically it would be designed to move a player. Clicking or touching in the middle of the screen causes a separate event, which for now causes a character to appear with a random color and location. But it could more practically be used to open a menu screen for additional commands – such as inventory, up/down, pick up, look, save, quit, etc.

Quality of Life

Some quality of life features have been added, such as the HELP command, and various color options (see: HELP in-game).

Terminal.js

A lot of major changes have occurred here.

class Terminal {
    constructor(cols, rows) {
        this.charWidth = 18;
        this.charHeight = 32;
        this.cols = cols;
        this.rows = rows;
        this.color = Color.ibm3270_green1;
        this.background = Color.ibm3270_green2;

        // Cursor metrics
        this.cx = 0;
        this.cy = 0;
        this.interval = 535; // 535ms
        this.cc = false;
        this.cursor = true;
        this.timerId = false;
        this.startCursorTimer();

        this.buf = new Array(rows);
        for (let y = 0; y < rows; y++) {
            this.buf[y] = new Array(cols);
            for (let x = 0; x < cols; x++) {
                this.buf[y][x] = new Character(' ', this.color, this.background);
            }
        }

        this.calcFontMetrics()
    }

    calcFontMetrics() {
        // Measure the width of a single character (assuming monospaced font)
        const measureTextResult = ctx.measureText('@');
        //console.log(measureTextResult);

        // Calculate the baseline offset
        // If there is a problem, adjust by hand.
        // PxPlus_IBM_VGA_9x16 seems to work well at 24...
        this.font_yadj = Math.round(32 - measureTextResult.fontBoundingBoxAscent) + 1;
        this.font_yadj = 24
        //console.log("Calculating font Y adjust: " + this.font_yadj);
    }

    // Function to schedule the timer to repeat
    startCursorTimer() {
        if (this.timerId == false) {
            this.timerId = setInterval(() => {
                this.cc = !this.cc;
                //console.log(`Cursor state: ${this.cc ? 'On' : 'Off'}`);
            }, this.interval);
        }
    }

    stopCursorTimer() {
        // Stopping the timer
        if (this.timerId) {
            clearInterval(this.timerId);
            this.timerId = null; // Optional: Set to null to indicate that the timer is not active
        }
    }

    setch(x, y, ch, color = this.color, background = this.background) {
        this.buf[y][x].ch = ch;
        this.buf[y][x].color = color;
        this.buf[y][x].background = background;
    }

//    setColor(c) {
//        this.color = c
//    }

//    setBkg(b) {
//        this.background = background
//    }

    type(key) {
        if (key.length === 1) {
            this.putch(key);
            return;
        }

        // It's nonstandard.
        if (key.length < 1) {
            return;
        }

        // Fallthrough: key is more than one character.
        if (key === "Enter") {
            let s = "";
            for (let i = 0; i < this.cols; i++) {
                let c = this.buf[this.cy][i].ch;
                s = s + c;
            }

            s = s.trim();
            s = s.toLowerCase();

            this.cr();
            this.lf();

            if (s === 'help') {
                this.puts("This is a technical demo of a terminal emulator in JavaScript.");
                this.cr();
                this.lf();
                this.puts("Help Commands: DATE, HELP, VERSION, WEBSITE");
                this.cr();
                this.lf();
                this.puts("Color Commands: NOTHEME, P1, P3, P3D, VGA");
                this.cr();
                this.lf();
                this.puts("Pressing ENTER on a line will enter that line as a console command.");
                this.cr();
                this.lf();
                this.puts("Press ESC to exit terminal mode and begin command processing.");
                this.cr();
                this.lf();
                this.puts("Press ESC again to return to terminal mode.");
                this.cr();
                this.lf();
                return;
            }

            if (s === 'version') {
                this.puts("JavaScript Terminal Technical Demo version 2.0");
                this.cr();
                this.lf();
                return;
            }

            if (s === 'date') {
                this.puts("Version 2.0, November 21st, 2023");
                this.cr();
                this.lf();
                return;
            }

            if (s === 'website') {
                this.puts("https://www.helloneo.ca/wiki/doku.php?id=javascript_terminal_v2");
                this.cr();
                this.lf();
                this.puts("helloneo.ca --> Wiki --> JavaScript Season 2");
                this.cr();
                this.lf();
            }

            if (s === 'p1') {
                console.log("changing theme to p1");
                this.setTheme('p1', Color.ibm3270_green1, Color.ibm3270_green2);
                return;
            }

            if (s === 'p3') {
                console.log("changing theme to p3");
                this.setTheme('p3', Color.ibm3270_amber1, Color.ibm3270_amber2);
                return;
            }

            if ((s === 'p3d') || (s === 'p3dark') || (s === 'p3-dark')) {
                console.log("changing theme to p3-dark");
                this.setTheme('p3', Color.ibm3270_amber1d4, Color.ibm3270_amber2d7);
                return;
            }


            if (s === 'vga') {
                console.log("changing theme to VGA");
                this.setTheme('vga', Color.lightgray, Color.black);
                return;
            }

            if (s ==='notheme') {
                console.log("changing theme to none");
                this.setTheme('', Color.lightgray, Color.black);
                return;
            }


            console.log('Pressed Enter on line ' + this.cy + ', found: "' + s + '".');
            console.log("adding event-command 'ENTER "  + s + "'");
            event_queue.push("ENTER " + s);
            return;
        }

        if (key === "Backspace") {
            this.arrowleft_jump();
            this.delete();
            return;
        }

        if (key === "Delete") {
            this.delete()
            return;
        }

        if (key === "ArrowLeft") {
            this.arrowleft();
            return;
        }

        if (key === "ArrowRight") {
            this.arrowright();
            return;
        }

        if (key === "ArrowUp") {
            this.cy = this.cy - 1;
            if (this.cy < 0) {
                this.cy = 0;
            }
            return;
        }

        if (key === "ArrowDown") {
            this.cy = this.cy + 1;
            if (this.cy >= this.rows) {
                this.cy = this.rows - 1;
            }
        }
        return;
    }

    putch(ch, color = this.color, background = this.background) {
        //console.log("putting " + ch + " at " + this.cx + "," + this.cy + " (max: "  + this.cols + "x" + this.rows + ")");

        if ((this.cx < 0) || (this.cx >= this.cols)) {
            console.log("putch oops: "  +this.cx + "," + this.cy)
            return;
        }
        this.buf[this.cy][this.cx].ch = ch;
        this.buf[this.cy][this.cx].color = color;
        this.buf[this.cy][this.cx].background = background;
        if (this.arrowright()) {
            this.cr();
            this.lf();
        }
    }

    puts(s) {
        for (let i = 0; i < s.length; i++) {
            let c = s.charAt(i);
            this.putch(c)
        }
    }

    putsxy(s, x, y) {
        this.cx = x;
        this.cy = y;
        this.puts(s);
    }

    delete() {
        if (this.cx >= this.cols) {
            return;
        }

        for (let i = this.cx; i < this.cols; i++) {
            // copy next into here;
            if ((i + 1) < this.cols) {
                this.buf[this.cy][i].ch = this.buf[this.cy][i + 1].ch;
                this.buf[this.cy][i].color = this.buf[this.cy][i + 1].color;
                this.buf[this.cy][i].background = this.buf[this.cy][i + 1].background;
                this.buf[this.cy][i + 1].ch = ' ';
                this.buf[this.cy][i + 1].color = this.color;
                this.buf[this.cy][i + 1].background = this.background;
            }
        }
    }

    backspace() {
        this.putch("Backspace");
    }

    arrowleft_jump() {
        var skip = this.arrowleft();

        while (skip) {
            if (this.buf[this.cy][this.cx].ch == ' ') {
                this.cx = this.cx - 1;
                if (this.cx < 0) {
                    this.cx = 0;
                    skip = false;
                }
            } else {
                skip = false;
                this.cx = this.cx + 1;
            }

        }
    }

    arrowleft() {
        this.cx = this.cx - 1;
        if (this.cx < 0) {
            this.cx = 0;
            if (this.cy > 0) {
                this.cy = this.cy - 1;
                this.cx = this.cols - 1;
                return true; // y - 1. signal true for arrowleft_jump() to skip spaces.
            }
        }
        return false; // didn't go up
    }

    arrowright() {
        this.cx = this.cx + 1;
        if (this.cx >= this.cols) {
            this.cx = 0;

            this.cy = this.cy + 1;
            if (this.cy >= this.rows) {
                this.cy = this.rows - 1;
                return true; // signal that a cr/lf is needed to 'go down'.
            } else {
                return false; // signal we handled the arrow in-terminal.
            }
        }
    }

    arrowup() {
        this.cy = this.cy - 1;
        if (this.cy < 0) {
            this.cy = 0
        }
    }

    arrowdown() {
        this.cy = this.cy + 1;
        if (this.cy >= this.rows) {
            this.cy = this.rows - 1;
        }
        // arrow downs on the bottom do nothing (they don't cause a lf).
    }

    cr() {
        this.cx = 0;
    }

    lf() {
        this.cy = this.cy + 1;
        if (this.cy >= this.rows) {
            this.cy = this.rows - 1;
            this.hard_lf()
        }
    }

    hard_lf() {
        for (let y = 1; y < this.rows; y++) {
            for (let x = 0; x < this.cols; x++) {
                this.buf[y-1][x].ch = this.buf[y][x].ch;
                this.buf[y-1][x].color = this.buf[y][x].color;
                this.buf[y-1][x].background = this.buf[y][x].background;
            }

        }

        for (let x = 0; x < this.cols; x++) {
            this.buf[this.rows-1][x].ch = ' ';
            this.buf[this.rows-1][x].color = this.color;
            this.buf[this.rows-1][x].background = this.background;
        }
        this.cx = 0;
        this.cy = this.rows - 1;
    }

    clearline(y) {
        if ((y < 0) || (y >= this.rows)) {
           console.log("invalid y in clearline(y), aborting.");
           return;
        }

        for (let x = 0; x < this.cols; x++) {
            this.buf[y][x].ch = ' ';
            this.buf[y][x].color = this.color;
            this.buf[y][x].background = this.background;
        }
    }

    drawCharacter(ctx, x, y, ch, color = this.color, background = this.background) {
        // It looks like we have to do this because of various factors.
        ctx.font = '32px myvga';

        // Calculate the actual position on the canvas based on character width and height
        const xPos = x * this.charWidth;
        const yPos = (y * this.charHeight);


        // Set the background
        ctx.fillStyle = background;
        ctx.fillRect(xPos, yPos, this.charWidth, this.charHeight);

        // Draw the character on the canvas
        ctx.fillStyle = color;
        ctx.fillText(ch, xPos, yPos + this.font_yadj);

    }

    draw(ctx) {
        for (let y = 0; y < this.rows; y++) {
            for (let x = 0; x < this.cols; x++) {
                // Get the character and color from the buf array
                const ch = this.buf[y][x].ch;
                const fg = this.buf[y][x].color;
                const bg = this.buf[y][x].background;
                this.drawCharacter(ctx, x, y, ch, fg, bg);
            }
        }

        // draw the cursor
        if (this.cursor && this.cc) {
            const xPos = this.cx * this.charWidth;
            const yPos = (this.cy * this.charHeight);
            ctx.fillStyle = this.color;
            ctx.fillText("\u005F", xPos, yPos + this.font_yadj);
        }
    }

    copyContentTo(newTerminal) {
        // Determine the number of rows and columns to copy
        const rowsToCopy = Math.min(this.rows, newTerminal.rows);
        const colsToCopy = Math.min(this.cols, newTerminal.cols);

        // Copy content from the old terminal to the new terminal
        for (let y = 0; y < rowsToCopy; y++) {
            for (let x = 0; x < colsToCopy; x++) {
                const nch = this.buf[y][x].ch;
                const ncolor = this.buf[y][x].color;
                const nbackground = this.buf[y][x].background;

                newTerminal.setch(x, y, nch, ncolor, nbackground);
            }
        }

        // Copy vital data
        newTerminal.cx = this.cx
        newTerminal.cy = this.cy
        newTerminal.cc = this.cc
        newTerminal.font_baseline = this.font_baseline;

        if (this.timerId) {
            this.stopCursorTimer()
            newTerminal.startCursorTimer()
        }

    }

    setTheme(name, fg, bg) {
        this.theme = name;
        this.color = fg;
        this.background = bg;

        if (name.length == 0) {
            return;
        }

        for (let y = 0; y < this.rows; y++) {
            for (let x = 0; x < this.cols; x++) {
                this.buf[y][x].color = fg;
                this.buf[y][x].background = bg;
            }

        }
    }
}

Code Discussion

Consistancy

From the top. this.color and this.background are used to provide consistancy. As an exercise to the reader, which will be implemented in v3, a character's foreground and background colors will be locked to the x,y character position. They will still be stored inside a single Character variable, but the logic will not change those values unless a special setfg or setbg series of functions are called. In terms of look and feel, ui, ux and that sort, it seems the right thing to do for game mode. For terminal mode, perhaps it should drag colors.

type()

The entry point for raw typed keys is type(). This is because we don't know what the user will type and he might type 'Enter' or 'Backspace', which are terminal control keys, not printable characters. So type() handles that from a terminal perspective. If the ch length is 1, it treats it like a printable character. If it's longer, it treats it as a control character (such as 'ArrowLeft').

Since we have the choice to either process lines directy in the type() function or create event-commands for later processing (such as when entering '10 print “hello”' and then typing RUN to enter event-command processing mode…) there are some commands which are processed directly, namely anything which requires an immediate response.

putch()

This is a real workhorse, which takes a character and puts it at the current cx,cy. It then handles the motion of the cursor, possibly executing a hard linefeed to scroll the screen up. This is the way in which we treat the screen as a lp device and just dump characters into it. Note that this is not intended to produce cr and lf or any special control or non-printable characters. Those should be brought in with the type() command instead. Also note that most people will use setch() instead to set the value of a character on the screen. In a practical sense, putch() is used mainly by puts().

puts() and putsxy()

These functions go through a string and putch() them, allowing putch() to handle desired things like wrapping to the next line. The operation is quite difficult and can fail if the string is too long for the line – a bug left as an exercise to the reader (but is intended to be fixed in v3).

backspace() and delete()

These and similar functions are designed to make the terminal and character editing on the termal feel like on a real terminal or word processor. They are intended to be as simple and basic as possible and solely to facilitate users being able to correct typing mistakes or make changes.

arrowleft() and arrowleft_jump()

These and other such commands as arrowup() control the motion of the cursor in a consistant way, the _jump() version skips spaces when wrapping up and to the far right, so it appears as if you cursor directly to the text. Most of the time this is the intended effect but we could also deny this and only call it if control is pressed during the arrowleft. There are a lot of UI/UX issues to consider, but we just want to keep things simple and small for now.

hard_lf()

This is the function that scrolls the screen up. On most terminals there's no scrollback buffer so we don't include one here. Our motto is “barebones”, or one might say (pun intended), “basic”.

drawCharacter()

It appears as if we need to set ctx.font = '32px myvga'; every time we call this. It is hard to predict timing issues (this is why waitForFont() was put into main.js) but in the end there are also context issues to consider. A simple assignment is as low-cost as we are going to get here. Unless you are coding a special system which requires custom control over when this is set to save cpu cycles, amd have dozens if not hundreds of hours to test and refine it, this is the best you are going to be able to do. It's not five nines; it's 100%. For what is likely the most oft-called function in the program it's worth it.

Color.js

Several new colors have been added. Standard greens and ambers based on phosphor wavelength values, as well as look-and-feel approximations of greens and ambers from an IBM 3270. Some look and feel ambers, and some phosphor and/or look and feel consensus opinions on the AppleII and AppleIIc have been added. In a v3, we may also decide to include more colors (such as for VIC-20, C64 and C128) and special fonts to simulate these terminals. For now the extra colors are there to demonstrate how to use the immediate command mode in Terminal (ex. by typing 'help').

Bugs

There were a few bugs. One early bug from v1 is that we have to keep setting the font when we draw tthe character, or for some reason it doesn't seem to trigger and can corrupt the screen.

Another bug is related to resizing the terminal. It may be better to simply regenerate the character array instead of creating a whole new terminal because a 'class' of bugs was introduced by not copying certain data properly and I had to keep going back and fixing it. There are probably still bugs remaining because of this, and the way to really fix them and also make the code simpler, is to merely regenerate the array in place. This will also give us the opportunity to change the font size on the fly, and so, it is slated for v3.

There is a 'bug' in the system message that if the screen is too small it wraps around and then is overwritten by other parts of the system message. This is a 'deal with it' thing, because I haven't bothered to put any checks in because that would be game dependant. This is just intended to be a barebones system – putting in checks for stuff like that is end user material.

There was, actually however, a bug in that system I did fix, if it is called with negative cx it doesn't do anything. I fixed that (I think). Actually that is also barebones stuff. Don't call it with negative CX. You (the systems programmer) should know – you have access to the width of the terminal. Use it.

In that sense there aren't really any other serious bugs, it basicaly works as intended (I checked). If it's something I didn't check then you will be able to work around it in the game code.

Closing Thoughts

This is an exciting platform to work with, once it is understood. To understand how to use this platform and how to extend it towards your game, I will discuss my plans for extending this code-base into JavaScript NetWhack. I will put this in a page JavaScript NetWhack.

javascript_terminal_v2.txt · Last modified: 2023/11/27 04:51 by appledog

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki