./exe/quest/readme.txt
Here is an arithmetic drilling program. It gives you five minutes to do as many arithmetic operations as you can in five minutes, cycling through the four major operations. You get more points for answering quickly, but the more points you have, the harder the questions get. You get a score at the end, which you can use to gauge your own progress across time.

You'll be surprised how quick your mind can get with practice...

Study the code and see how it is built. Missing is the code for the terminal interface "xterm.js" which is available on github. It is just the interface to print characters to the screen; all the game logic is here.
./exe/quest/main.html
<!DOCTYPE HTML>

<html>
	<head>

		<link rel="stylesheet" href="../xterm/css/xterm.css" /> 
		<script src="../xterm/lib/xterm.js"></script> 

		<meta charset="UTF-8">   
		<link rel="stylesheet" href="style.css">
		
	</head>
	
	<body>
		<div>
			<canvas id = "progress_bar"></canvas>
		</div>
		<div id="terminal"></div>

		<script src="game.js"></script>
	</body>
</html>
  
  
./exe/quest/style.css
body { 	
	background-color: #336699; 
	scrollbar-width: none;
	overflow: hidden;
}
div {
	scrollbar-width: none;
	cursor: crosshair;
	overflow: hidden;
}
canvas {
	padding-left: 0;
	padding-right: 0;
	margin-left: auto;
	margin-right: auto;
	display: block;
}


./exe/quest/game.js
	//------------------------------------------------------Global Flags
	//TODO these can probably be collapsed into one
	var ready = false;
	var between_games = false;

	//------------------------------------Initialization of progress bar	
	var canvas = document.getElementById('progress_bar');
	var context = canvas.getContext('2d');

	const urlParams = new URLSearchParams(window.location.search);
	var mins = urlParams.get('mins');
	var debug = urlParams.get('debug');
	if (isNaN(parseInt(mins))) mins = 5;
	else mins = parseInt(mins);
	var TIME_LIMIT = 1000 * 60 * mins;

	//-------------------------------------------------------Progress Bar Stuff
	var START_TIME = -1; // temporarily negative before game starts	

	function computeDecimalFull(time) {
		if (START_TIME < 0) return 0;
		return Math.min(1, (time - START_TIME)/TIME_LIMIT);
	}

	let color1 = '#4499EE';
	let color2 = '#255585';
	function progress_bar(){
		const decimalFull = computeDecimalFull(Date.now());
		const widthFull = decimalFull*canvas.width;
		context.fillStyle = color1;
		context.fillRect(0,0, widthFull, canvas.height);
		context.fillStyle = color2;
		context.fillRect(widthFull, 0, canvas.width-widthFull, canvas.height);
		if (debug == '1'){
			context.fillStyle = '#000';
			context.fillText(level.toString(),10,10);
		}
		if (isTimeOut()){
			ready = false;
			between_games = true;
			clearInterval(id);
			term.writeln("\n\n\rlevel: " + level.toString());
			term.writeln("\x1b[95m<enter> again?");
			term.writeln("  or <escape>?\x1b[97m");
		}
	}

	function isTimeOut() { return START_TIME + TIME_LIMIT < Date.now();}

	window.addEventListener("resize", draw_canvas);
	const PROGRESS_BAR_SCALE = 1.1;
	function draw_canvas(){ //rename to resize?
		var context = canvas.getContext('2d');
		canvas.setAttribute('width', window.innerWidth-30); 
		//canvas.setAttribute('height', window.innerHeight*PROGRESS_BAR_SCALE);//TODO
		canvas.setAttribute('height', 100);
		canvas.width = canvas.width; canvas.height = canvas.height;
		progress_bar();
	}
	const NUM_COLS = 16;
	const NUM_ROWS = 4;
	const digits = "0123456789";
	var level = 1;
	var quest_start;
	
	var quest = {}; // "object oriented programming"

	// random normally distributed variable: sd=1 mean=0
	function randn_bm() {
	    var u = 0, v = 0;
	    while(u === 0) u = Math.random(); //Converting [0,1) to (0,1)
	    while(v === 0) v = Math.random();
	    return Math.sqrt(-2*Math.log(u)) * Math.cos(2*Math.PI*v);
	}	

	//TODO color code the op chars?
	function generate_add(level){
		quest.a 	= Math.abs(Math.round(level + level/2*randn_bm()));
		quest.b 	= Math.abs(Math.round(level + level/2*randn_bm()));
		quest.ans 	= quest.a + quest.b;
		quest.op 			= '+';
		generate_prompt();
	}

	function generate_mul(level){
		quest.a 	= Math.abs(Math.round(level*(2/3) + 10*randn_bm()));
		quest.b 	= 1 + Math.ceil(Math.random()*12);
		quest.ans 	= quest.a * quest.b;
		quest.op			= 'x';
		generate_prompt();
	}

	function generate_sub(level){
		quest.b 	= Math.abs(Math.round(level + 10*randn_bm()));
		quest.ans 	= Math.round(2*level*Math.random());
		quest.a 	= quest.ans + quest.b; 
		quest.op 			= '―';
		generate_prompt();
	}

	function generate_div(level){
		quest.a = level*11;
		while (quest.a > level*10){
			quest.ans 	= Math.abs(Math.round(level/3 + 10*randn_bm())); 
			quest.b 	= 2 + Math.ceil(level*Math.random()/3);
			quest.a 	= quest.b * quest.ans;
		}
		quest.op 			= '÷';
		generate_prompt();
	}

	var op_counter = 0;
	//TODO make the op_cycle dependent on URL parameters
	const op_cycle = [generate_add, generate_mul, generate_sub, generate_div];
	
	function generate_prompt(){
		quest.widest = Math.max(	quest.a.toString().length, 
						quest.b.toString().length,
						quest.ans.toString().length);
		quest.bar 			= '―'.repeat(quest.widest + 2);
		quest.top_cushion 	= ' '.repeat(quest.widest - quest.a.toString().length + 2);
		quest.bot_cushion 	= ' '.repeat(quest.widest - quest.b.toString().length + 1);
		quest.ans_cushion	= ' '.repeat(quest.widest - quest.ans.toString().length + 2);
		quest.margin 		= ' '.repeat(Math.floor((NUM_COLS - quest.widest)/2));
	}
	
	function PROMPT() {
		return 	quest.margin 				+ quest.top_cushion + quest.a + "\n\r" + 
				quest.margin + quest.op 	+ quest.bot_cushion + quest.b + "\n\r" +
				quest.margin + quest.bar	+ "\n\r"+ 
				quest.margin 				+ quest.ans_cushion; 	
	}
	
	function score(time_taken) {
		if (time_taken < 1000 + level*10)
			return 5
		if (time_taken < 2000 + level*10)
			return 3
		if (time_taken < 3000 + level*10)
			return 2
		return 1
	}

	function prompt(term) { term.write('\n\r' + PROMPT()); }	

	function check(ans) {

		if (parseInt(ans) == quest.ans){
			time_taken = Date.now() - quest_start;
			level += score(time_taken);	
			op_cycle[(op_counter++)%op_cycle.length](level);
			quest_start = Date.now();
		}
	}

	//--------------------------------------------------------------Terminal settings
	/*TODO I think I want to make my own terminal emulator and use
	 * it instead of the term.js thing. It's much bulkier than what
	 * I need, both memory-wise and in the number of features. 
	 * I need to use something lighter, that basically only does put_char()
	 * and get_line(). I will have a much easier time scaling it since
	 * html canvas 
	 */

	const term = new Terminal( // takes object as perameter (see docs)
		{ 
			theme: {
				background: "#336699",
			},
			rows: NUM_ROWS,
			cols: 40,
			cursorBlink: false,
			// this upcoming font size is a total guess, it seems to fit on a 
			// standard screen width but does not handle resizes well. 
			// another thing that making an emulator from scratch would free me from
			fontSize: Math.floor(innerHeight/ (NUM_ROWS*2)),
			fontWeight: 900
		});

	var buffer = []; // TODO: might be accessible directly from term...
	
	function reset() {
		return 0;
	}

	//--------------------------------------------Open terminal and initiate listeners	
	
	term.open(document.getElementById('terminal'));
	//term.write('\x1b[95m') // font color
	term.onKey(e => { // this listener can be linked to anything, even the canvas terminal
		const printable = !e.domEvent.altKey && !e.domEvent.altGraphKey 
			       && !e.domEvent.ctrlKey && !e.domEvent.metaKey;
		
		// ENTER pressed
		if (e.domEvent.keyCode === 13) {
			if (!ready) { // put this in reset() subroutine
				ready = true;
				level = 1;
				op_cycle[(op_counter++)%op_cycle.length](level);
				START_TIME = Date.now();
				quest_start = START_TIME; // First question
				id = setInterval(draw_canvas, 50); 
			}

			if (between_games){
				between_games = false;
			}

			// if the answer is wrong, don't send it?
			check(buffer.join(''));
			buffer = [];
			if (!isTimeOut()) prompt(term);
		
		// BACKSPACE
		} else if (e.domEvent.keyCode === 8 && 
		term._core.buffer.x > (quest.ans_cushion.length + 
							   quest.margin.length)){
				buffer.pop();
				term.write('\b \b');
			
		// LEGIT INPUT
		} else if (	digits.includes(e.key) && 
					(buffer.length < quest.ans.toString().length)){
			buffer.push(e.key);
			term.write(e.key);

		// FULL SCREEN
		} else if ("Ff".includes(e.key)){
			openFullscreen();
			term.scrollToBottom();
		}
	});

	//----------------------------------------------------------------------------------
	//TODO control both width and height. keep it at a 2:3 ratio or something
	onresize = function() { 
		// make it minimum with a width-based measurement
		term.setOption("fontSize", Math.floor(innerHeight*4/(NUM_ROWS*6)));
		term.clear();
		if (ready) prompt(term);
	}

	window.onload = function() {
		term.focus();
		term.write("Calculator Quest");
		term.write("\n\r\x1b[95m<f> : fullscreen \n\r<enter> : begin\x1b[97m");
	};
	
	var elem = document.documentElement;
	/* View in fullscreen */
	function openFullscreen() {
		if (elem.requestFullscreen) {
			elem.requestFullscreen();
		} else if (elem.mozRequestFullScreen) { /* Firefox */
			elem.mozRequestFullScreen();
		} else if (elem.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
			elem.webkitRequestFullscreen();
		} else if (elem.msRequestFullscreen) { /* IE/Edge */
			elem.msRequestFullscreen();
		}
	}


./exe/quest/.terminal.js

	var elem = document.documentElement;

	/* View in fullscreen */
	function openFullscreen() {
		if (elem.requestFullscreen) {
			elem.requestFullscreen();
		} else if (elem.mozRequestFullScreen) { /* Firefox */
			elem.mozRequestFullScreen();
		} else if (elem.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
			elem.webkitRequestFullscreen();
		} else if (elem.msRequestFullscreen) { /* IE/Edge */
			elem.msRequestFullscreen();
		}
	}

	/* Close fullscreen */
	function closeFullscreen() {
		if (document.exitFullscreen) {
			document.exitFullscreen();
		} else if (document.mozCancelFullScreen) { /* Firefox */
			document.mozCancelFullScreen();
		} else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */
			document.webkitExitFullscreen();
		} else if (document.msExitFullscreen) { /* IE/Edge */
			document.msExitFullscreen();
		}
	}

	const NUM_COLS = 16;
	const NUM_ROWS = 4;
	const digits = "0123456789";
	var level = 5;
	var question_start;
	
	var question = {};
	function generate_add(level){
		question.a = Math.round(2*level*Math.random());
		question.b = Math.round(2*level*Math.random());
		question.ans = question.a + question.b;
		question.widest = Math.max(	question.a.toString().length, 
						question.b.toString().length,
						question.ans.toString().length);
		question.op = '+';
		question.bar = '―'.repeat(question.widest + 1);
		question.top_cushion = ' '.repeat(question.widest - question.a.toString().length + 1);
		question.bot_cushion = ' '.repeat(question.widest - question.b.toString().length);
		question.ans_cushion = ' '.repeat(question.widest - question.ans.toString().length + 1);
		question.margin = ' '.repeat(Math.floor((NUM_COLS - question.widest)/2));

	}

	generate_add(level); // first question
	question_start = Date.now();
	
	function PROMPT() {
		return 	question.margin + 		question.top_cushion + question.a.toString() +"\n\r"+ 
			question.margin + question.op + 	question.bot_cushion + question.b.toString() + "\n\r"+
			question.margin + question.bar +  "\n\r"+ 
			question.margin + 		question.ans_cushion; 	
			
	}
	
	function score(time_taken) {
		return 1;
	}

	function prompt(term) { term.write('\n\r' + PROMPT()); }	

	function check(ans) {
		if (parseInt(ans) == question.ans){
			time_taken = Date.now() - question_start;
			level += score(time_taken);	
			generate_add(level)
			if (isTimeOut())
				term.write("out of time");;
		}
	}

	//--------------------------------------------------------------Terminal settings

	const term = new Terminal( // takes object as perameter (see docs)
		{ 
			theme: {
				background: "#336699",
			},
			rows: NUM_ROWS,
			cols: 40,
			cursorBlink: false,
			fontSize: Math.floor(innerHeight/ (NUM_ROWS*2)),
			fontWeight: 900
		});

	var buffer = []; // TODO: might be accessible directly from term...
	
	//--------------------------------------------Open terminal and initiate listeners	

	term.open(document.getElementById('terminal'));
	term.write('\x1b[97m') // font color
	term.onKey(e => {
		const printable = !e.domEvent.altKey && !e.domEvent.altGraphKey 
			       && !e.domEvent.ctrlKey && !e.domEvent.metaKey;

		if (e.domEvent.keyCode === 13) { // ENTER pressed
			if (!ready) {ready = true;
				START_TIME = Date.now();
				setInterval(draw_canvas, 100); }
			check(buffer.join(''));
			buffer = []; // reset buffer
			prompt(term);
		} else if (e.domEvent.keyCode === 8) { // Backspace pressed
			if (term._core.buffer.x >= (question.ans_cushion.length + question.margin.length)){
				buffer.pop();
				term.write('\b \b');
			}
		} else if (digits.includes(e.key) && buffer.length < question.ans.toString().length) { // standard character
			buffer.push(e.key);
			term.write(e.key);
		} else if ("Ff".includes(e.key)){
			openFullscreen();
			term.scrollToBottom();
		}

	});

	//TODO control both width and height. keep it at a 2:3 ratio or something
	onresize = function() { term.setOption("fontSize",
				Math.floor(innerHeight*4/(NUM_ROWS*6)));
		term.clear();
		prompt(term);
	}

	//----------------------------------------------------------------------------------


	window.onload = function() {
		term.focus();
		term.write("Press ⓕ \n\r then ENTER");
	};