diff --git a/xtrn/jeopardized/jeopardized.js b/xtrn/jeopardized/jeopardized.js
new file mode 100644
index 0000000000000000000000000000000000000000..706884cf937c787c8a6550812566caaafcae05d1
--- /dev/null
+++ b/xtrn/jeopardized/jeopardized.js
@@ -0,0 +1,234 @@
+load('sbbsdefs.js');
+load('nodedefs.js');
+load('http.js');
+load('frame.js');
+load('tree.js');
+load('layout.js');
+load('event-timer.js');
+load('json-client.js');
+load('typeahead.js');
+load('scrollbar.js');
+
+load(js.exec_dir + 'lib/defs.js');
+load(js.exec_dir + 'lib/func.js');
+load(js.exec_dir + 'lib/frame-ext.js');
+load(js.exec_dir + 'lib/database.js');
+load(js.exec_dir + 'views/messages.js');
+load(js.exec_dir + 'views/menu.js');
+load(js.exec_dir + 'views/game.js');
+load(js.exec_dir + 'views/board.js');
+load(js.exec_dir + 'views/wager.js');
+load(js.exec_dir + 'views/clue.js');
+load(js.exec_dir + 'views/answer.js');
+load(js.exec_dir + 'views/round.js');
+load(js.exec_dir + 'views/popup.js');
+load(js.exec_dir + 'views/scoreboard.js');
+load(js.exec_dir + 'views/news.js');
+load(js.exec_dir + 'views/bin-scroller.js');
+
+var database,
+	frame,
+	menu,
+	game,
+	news,
+	help,
+	credits,
+	settings,
+	consoleAttr,
+	sysStatus,
+	popUp,
+	scoreboard,
+	buryCursor = false;
+
+var state = STATE_MENU;
+
+function loadSettings() {
+	settings = {};
+	var f = new File(js.exec_dir + 'settings.ini');
+	f.open('r');
+	settings.JSONDB = f.iniGetObject('JSONDB');
+	settings.WebAPI = f.iniGetObject('WebAPI');
+	f.close();
+}
+
+function initDatabase() {
+
+	database = new Database(settings.JSONDB);
+
+	database.on(
+		'state',
+		function (update) {
+			if (update.data.locked) state = STATE_MAINTENANCE;
+		}
+	);
+
+	if (database.getState().locked) {
+		console.clear(WHITE);
+		console.putmsg('Maintenance in progress.  Exiting ...\r\n');
+		console.pause();
+		state = STATE_MAINTENANCE;
+	} else {
+		database.notify('\1n\1c' + user.alias + ' is here.', false);
+	}
+
+}
+
+function initDisplay() {
+	console.clear(WHITE);
+	frame = new Frame(1, 1, 80, 24, WHITE);
+	frame.checkbounds = false;
+	frame.centralize();
+	frame.open();
+	menu = new Menu(frame);	
+}
+
+function init() {
+	consoleAttr = console.attributes;
+	sysStatus = bbs.sys_status;
+	loadSettings();
+	initDatabase();
+	initDisplay();
+}
+
+function main() {
+
+	while (state !== STATE_QUIT && state !== STATE_MAINTENANCE) {
+
+		database.cycle();
+
+		var cmd = console.inkey(K_NONE, 5);
+		if (cmd === '/') break;
+
+		switch (state) {
+
+			case STATE_MENU:
+				var ret = menu.getcmd(cmd);
+				menu.cycle();
+				if (typeof ret !== 'boolean') {
+					state = ret;
+				} else if (typeof ret === 'boolean' && ret) {
+					buryCursor = true;
+				}
+				break;
+
+			case STATE_GAME:
+				if (typeof game === 'undefined') game = new Game(frame);
+				var ret = game.getcmd(cmd);
+				game.cycle();
+				if (typeof ret === 'boolean' && !ret) {
+					state = STATE_MENU;
+					game.close();
+					game = undefined;
+				}
+				break;
+
+			case STATE_SCORE:
+				if (typeof scoreboard === 'undefined') {
+					scoreboard = new Scoreboard(frame);
+				}
+				var ret = scoreboard.getcmd(cmd);
+				scoreboard.cycle();
+				if (!ret) {
+					state = STATE_MENU;
+					scoreboard.close();
+					scoreboard = undefined;
+				}
+				break;
+
+			case STATE_NEWS:
+				if (typeof news === 'undefined') news = new News(frame);
+				var ret = news.getcmd(cmd);
+				news.cycle();
+				if (!ret) {
+					state = STATE_MENU;
+					news.close();
+					news = undefined;
+				}
+				break;
+
+			case STATE_HELP:
+				if (typeof help === 'undefined') {
+					help = new BinScroller(
+						frame,
+						js.exec_dir + 'views/help.bin',
+						77,
+						53
+					);
+				}
+				var ret = help.getcmd(cmd);
+				help.cycle();
+				if (!ret) {
+					state = STATE_MENU;
+					help.close();
+					help = undefined;
+				}
+				break;
+
+			case STATE_CREDIT:
+				if (typeof credits === 'undefined') {
+					credits = new BinScroller(
+						frame,
+						js.exec_dir + 'views/credits.bin',
+						77,
+						20
+					);
+				}
+				var ret = credits.getcmd(cmd);
+				credits.cycle();
+				if (!ret) {
+					state = STATE_MENU;
+					credits.close();
+					credits = undefined;
+				}
+				break;
+
+			case STATE_MAINTENANCE:
+				state = STATE_POPUP;
+				popUp = new PopUp(
+					frame,
+					'Maintenance is in progress.  Exiting ...',
+					'[Press any key to continue]',
+					PROMPT_ANY,
+					STATE_MAINTENANCE
+				);
+				break;
+
+			case STATE_POPUP:
+				popUp.cycle();
+				if (typeof popUp.getcmd(cmd) !== 'undefined') {
+					state = popUp.close();
+				}
+				break;
+
+			default:
+				break;
+
+		}
+
+		if (frame.cycle() || buryCursor) {
+			console.gotoxy(console.screen_columns, console.screen_rows);
+			buryCursor = false;
+		}
+
+	}
+
+}
+
+function cleanUp() {
+	if (typeof database !== 'undefined') {
+		database.notify('\1n\1c' + user.alias + ' has left.', false);
+		database.close();
+	}
+	if (typeof frame !== 'undefined') frame.close();
+	console.clear(consoleAttr);
+	bbs.sys_status = sysStatus;
+}
+
+try {
+	init();
+	main();
+} catch (err) {
+	log(LOG_ERR, err);
+} finally {
+	cleanUp();
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/lib/database.js b/xtrn/jeopardized/lib/database.js
new file mode 100644
index 0000000000000000000000000000000000000000..0f64280344749c0d869ffd09b0be0be41a43bc1a
--- /dev/null
+++ b/xtrn/jeopardized/lib/database.js
@@ -0,0 +1,301 @@
+var Database = function (settings) {
+
+	var self = this;
+	var jsonClient;
+	var callbacks = {}; // { 'location' : [ function (update) {} ] ... }
+	var attempts = 0;
+
+	function addUser(usr) {
+
+		var id = self.getUserID(usr);
+
+		var update = {
+			key : 'users',
+			data : {
+				id : id,
+				alias : usr.alias,
+				system : system.name
+			}
+		};
+		jsonClient.write(settings.dbName, 'input', update, 2);
+
+		var u;
+		for (var n = 0; n < settings.retries; n++) {
+			mswait(settings.retryDelay);
+			u = jsonClient.read(settings.dbName, 'users.' + id, 1);
+			if (typeof u !== 'undefined') break;
+		}
+
+		return u;
+
+	}
+
+	function addUserGameState(usr, round) {
+
+		var update = {
+			key : 'game.users',
+			data : { id : self.getUserID(usr) }
+		};
+		jsonClient.write(settings.dbName, 'input', update, 2);
+
+		var ugs;
+		for (var n = 0; n < settings.retries; n++) {
+			mswait(settings.retryDelay);
+			ugs = jsonClient.read(
+				settings.dbName,
+				'game.users.' + self.getUserID(usr),
+				1
+			);
+			if (typeof ugs !== 'undefined') break;
+		}
+
+		return ugs;
+
+	}
+
+	this.getUserID = function (usr) {
+		return base64_encode(usr.alias + '@' + system.name);
+	}
+
+	this.getState = function () {
+		return jsonClient.read(settings.dbName, 'state', 1);
+	}
+
+	this.getRound = function (round) {
+		var r = jsonClient.read(settings.dbName, 'game.round.' + round, 1);
+		if (typeof r === 'object') r.number = round;
+		return r;
+	}
+
+	this.getUser = function (usr) {
+		var u = jsonClient.read(
+			settings.dbName,
+			'users.' + self.getUserID(usr),
+			1
+		);
+		if (typeof u === 'undefined') return addUser(usr);
+		return u;
+	}
+
+	this.getUserGameState = function (usr) {
+		var ugs = jsonClient.read(
+			settings.dbName,
+			'game.users.' + self.getUserID(usr),
+			1
+		);
+		if (typeof ugs === 'undefined') return addUserGameState(usr);
+		return ugs;
+	}
+
+	this.markClueAsUsed = function (usr, round, category, clue) {
+		var update = {
+			key : 'game.state',
+			data : {
+				id : self.getUserID(usr),
+				round : round,
+				category : category,
+				clue : clue
+			}
+		};
+		return jsonClient.write(settings.dbName, 'input', update, 2);
+	}
+
+	this.submitAnswer = function (usr, round, category, clue, answer, wager) {
+		var update = {
+			key : 'game.answer',
+			data : {
+				id : self.getUserID(usr),
+				round : round,
+				category : category,
+				clue : clue,
+				answer : answer,
+				value : wager
+			}
+		};
+		return jsonClient.write(settings.dbName, 'input', update, 2);
+	}
+
+	this.reportClue = function (usr, clue, answer) {
+		var update = {
+			key : 'game.report',
+			data : {
+				userID : self.getUserID(usr),
+				clueID : clue.id,
+				userAnswer : answer,
+				realAnswer : clue.answer
+			}
+		};
+		return jsonClient.write(settings.dbName, 'input', update, 2);
+	}
+
+	this.nextRound = function (usr) {
+		var update = {
+			key : 'game.nextRound',
+			data : {
+				id : self.getUserID(usr)
+			}
+		};
+		return jsonClient.write(settings.dbName, 'input', update, 2);
+	}
+
+	this.postMessage = function (message, usr) {
+		var update = {
+			key : 'messages',
+			data : {
+				alias : usr.alias,
+				system : system.name,
+				message : message,
+				store : true
+			}
+		};
+		return jsonClient.write(settings.dbName, 'input', update, 2);
+	}
+
+	this.notify = function (message, store) {
+		var update = {
+			key : 'messages',
+			data : {
+				system : system.name,
+				store : store,
+				message : message
+			}
+		}
+		return jsonClient.write(settings.dbName, 'input', update, 2);
+	}
+
+	this.getMessages = function () {
+		var messages = jsonClient.read(
+			settings.dbName, 'messages.history', 1
+		);
+		return messages;
+	}
+
+	this.getNews = function (count) {
+		if (typeof count === 'undefined') count = 50;
+		return jsonClient.slice(settings.dbName, 'news', 0, 0 + count, 1);
+	}
+
+	this.getRankings = function () {
+		var ret = { 
+			today : { data : {}, money : [] }, 
+			total : { data : {}, money : [] }
+		};
+		var today = jsonClient.read(settings.dbName, 'game.users', 1);
+		var total = jsonClient.read(settings.dbName, 'users', 1);
+		Object.keys(today).forEach(
+			function (key) {
+				var r = { 
+					id : key,
+					winnings : today[key].winnings,
+					answers : today[key].answers
+				};
+				ret.today.money.push(key);
+				ret.today.data[key] = r;
+			}
+		);
+		Object.keys(total).forEach(
+			function (key) {
+				ret.total.money.push(key);
+				ret.total.data[key] = {
+					id : key,
+					winnings : total[key].winnings,
+					answers : total[key].answers
+				};
+			}
+		);
+		ret.today.money.sort(
+			function (a, b) {
+				return ret.today.data[b].winnings - ret.today.data[a].winnings;
+			}
+		);
+		ret.total.money.sort(
+			function (a, b) {
+				return ret.total.data[b].winnings - ret.total.data[a].winnings;
+			}
+		);
+		return ret;
+	}
+
+	this.who = function () {
+		var who = jsonClient.who(settings.dbName, 'state');
+		if (!who || !Array.isArray(who)) return [];
+		return who;
+	}
+
+	this.on = function (location, callback) {
+		if (typeof location !== 'string' || typeof callback !== 'function') {
+			return -1;
+		}
+		if (typeof callbacks[location] === 'undefined') {
+			jsonClient.subscribe(settings.dbName, location);
+			callbacks[location] = [callback];
+		} else {
+			callbacks[location].push(callback);
+		}
+		return callbacks.length - 1;
+	}
+
+	this.off = function (location, index) {
+		if (typeof location !== 'string' || typeof index !== 'number') return;
+		if (typeof callbacks[location] === 'undefined') return;
+		if (index < 0 || index > callbacks[location].length) return;
+		callbacks[location][index] = null;
+	}
+
+	this.connect = function () {
+
+		jsonClient = new JSONClient(settings.host, settings.port);
+
+		Object.keys(callbacks).forEach(
+			function (key) {
+				jsonClient.subscribe(settings.dbName, key);
+			}
+		);
+
+		jsonClient.callback = function (update) {
+			log(JSON.stringify('update'));
+			if (update.func !== 'UPDATE' || update.oper !== 'WRITE') return;
+			if (!Array.isArray(callbacks[update.location])) return;
+			callbacks[update.location].forEach(
+				function (callback) {
+					if (typeof callback !== 'function') return;
+					callback(update);
+				}
+			);
+		}
+
+	}
+
+	this.close = function () {
+		Object.keys(callbacks).forEach(
+			function (location) {
+				jsonClient.unsubscribe(settings.dbName, location);
+				callbacks[location].forEach(
+					function (callback, index) {
+						if (typeof callback === 'function') {
+							self.off(location, index)
+						}
+					}
+				);
+			}
+		);
+	}
+
+	this.cycle = function () {
+		if (!jsonClient.connected) {
+			throw 'Disconnected from server.';
+		} else {
+			jsonClient.cycle();
+		}
+	}
+
+	this.__defineGetter__(
+		'connected',
+		function () {
+			return jsonClient.connected;
+		}
+	);
+
+	this.connect();
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/lib/defs.js b/xtrn/jeopardized/lib/defs.js
new file mode 100644
index 0000000000000000000000000000000000000000..d6ac638e68c2cd713f218f710e67b04111baa790
--- /dev/null
+++ b/xtrn/jeopardized/lib/defs.js
@@ -0,0 +1,43 @@
+// Main module states
+const	STATE_MENU = 0,
+		STATE_GAME = 1,
+		STATE_SCORE = 2,
+		STATE_NEWS = 3,
+		STATE_HELP = 4,
+		STATE_CREDIT = 5,
+		STATE_QUIT = 6,
+		STATE_MAINTENANCE = 7,
+		STATE_POPUP = 8;
+
+// Menu module states
+const	STATE_MENU_TREE = 0,
+		STATE_MENU_MESSAGES = 1;
+
+// Game module states
+const	STATE_GAME_PLAY = 0,
+		STATE_GAME_MESSAGES = 1,
+		STATE_GAME_POPUP = 2;
+
+// Round module states
+const 	STATE_ROUND_BOARD = 0,
+		STATE_ROUND_CLUE = 1,
+		STATE_ROUND_ANSWER = 2,
+		STATE_ROUND_POPUP = 3,
+		STATE_ROUND_WAGER = 4,
+		STATE_ROUND_COMPLETE = 5;
+
+// Clue module states
+const	STATE_CLUE_INPUT = 0;
+
+// PopUp prompt modes
+const	PROMPT_ANY = 0,
+		PROMPT_YN = 1,
+		PROMPT_TEXT = 2,
+		PROMPT_NUMBER = 3;
+
+// Round module return values
+const	RET_ROUND_QUIT = 0,		// User opted to quit
+		RET_ROUND_CONTINUE = 1, // Clues available & can't advance yet
+		RET_ROUND_NEXT = 2,		// Can advance to next round
+		RET_ROUND_STOP = 3,		// No more clues & can't advance
+		RET_ROUND_LAST = 4;		// Round 3 complete
\ No newline at end of file
diff --git a/xtrn/jeopardized/lib/frame-ext.js b/xtrn/jeopardized/lib/frame-ext.js
new file mode 100644
index 0000000000000000000000000000000000000000..d0a986d97d5dfa675b3a25eb76224c3e966572fc
--- /dev/null
+++ b/xtrn/jeopardized/lib/frame-ext.js
@@ -0,0 +1,106 @@
+/*	'color' may be a colour def (LIGHTBLUE, etc.) or an array of same
+	'title', if present, must have x, y, attr, and text properties */
+Frame.prototype.drawBorder = function(color, title) {
+	this.pushxy();
+	var theColor = color;
+	if (Array.isArray(color)) {
+		var sectionLength = Math.round(this.width / color.length);
+	}
+	for (var y = 1; y <= this.height; y++) {
+		for (var x = 1; x <= this.width; x++) {
+			if (x > 1 && x < this.width && y > 1 && y < this.height) continue;
+			var msg;
+			this.gotoxy(x, y);
+			if (y === 1 && x === 1) {
+				msg = ascii(218);
+			} else if (y === 1 && x === this.width) {
+				msg = ascii(191);
+			} else if (y === this.height && x === 1) {
+				msg = ascii(192);
+			} else if (y === this.height && x === this.width) {
+				msg = ascii(217);
+			} else if (x === 1 || x === this.width) {
+				msg = ascii(179);
+			} else {
+				msg = ascii(196);
+			}
+			if (Array.isArray(color)) {
+				if (x === 1) {
+					theColor = color[0];
+				} else if (x % sectionLength === 0 && x < this.width) {
+					theColor = color[x / sectionLength];
+				} else if (x === this.width) {
+					theColor = color[color.length - 1];
+				}
+			}
+			this.putmsg(msg, theColor);
+		}
+	}
+	if (typeof title === 'object') {
+		this.gotoxy(title.x, title.y);
+		this.attr = title.attr;
+		this.putmsg(ascii(180) + title.text + ascii(195));
+	}
+	this.popxy();
+}
+
+/*	Word-wrap and centre a string that may span multiple lines, and may already
+	be multi-line itself. */
+Frame.prototype.centerWrap = function (str) {
+	var self = this;
+	var arr = [''];
+	str.split('\r\n').forEach(
+		function (line, i, a) {
+			line.split(' ').forEach(
+				function (word) {
+					if ((arr[arr.length - 1] + ' ' + word).length <=
+							self.width
+					) {
+						arr[arr.length - 1] += (' ' + word);
+					} else if (word.length > self.width) {
+						arr.push(word.substr(0, self.width - 1) + '-');
+						arr.push(word.substr(self.width - 1));
+					} else {
+						arr.push(word);
+					}
+				}
+			);
+			if (i < a.length - 1) arr.push('');
+		}
+	);
+	arr.forEach(
+		function (word, i, a) {
+			self.center(skipsp(truncsp(word)));
+			if (i < a.length - 1) self.crlf();
+		}
+	);
+}
+
+// Center this frame within other frame 'p', or the terminal if 'p' is omitted
+Frame.prototype.centralize = function (p) {
+	if (typeof p === 'undefined') {
+		var p = {
+			x : 1,
+			y : 1,
+			width : console.screen_columns,
+			height : console.screen_rows
+		};
+	}
+	var xy = {
+		x : p.x + Math.floor((p.width - this.width) / 2),
+		y : p.y + Math.floor((p.height - this.height) / 2)
+	};
+	this.moveTo(xy.x, xy.y);
+}
+
+/*	Expand to the size of the parent frame, optionally leaving x and / or
+	y padding between this frame and its parent. */
+Frame.prototype.nest = function (paddingX, paddingY) {
+	if (typeof this.parent === 'undefined') return;
+	var xOffset = (typeof paddingX === 'number' ? paddingX : 0);
+	var yOffset = (typeof paddingY === 'number' ? paddingY : 0);
+	this.x = this.parent.x + xOffset;
+	this.y = this.parent.y + yOffset;
+	this.width = this.parent.width - (xOffset * 2);
+	this.height = this.parent.height - (yOffset * 2);
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/lib/func.js b/xtrn/jeopardized/lib/func.js
new file mode 100644
index 0000000000000000000000000000000000000000..f5b977744c61f04378450b82d8a1fac1dcaf78a1
--- /dev/null
+++ b/xtrn/jeopardized/lib/func.js
@@ -0,0 +1,37 @@
+// Where 'game' is a given user's game state, and 'round' is a number
+function countAnswers(game, round) {
+	var answers = 0;
+	Object.keys(game.rounds[round]).forEach(
+		function (category) {
+			answers += game.rounds[round][category].length;
+		}
+	);
+	return answers;
+}
+
+function compareAnswer(id, answer) {
+	var res = JSON.parse(
+		(new HTTPRequest()).Get(
+			settings.WebAPI.url + '/clues/' + id + '/compare/' +
+			encodeURIComponent(answer)
+		)
+	);
+	return res.correct;
+}
+
+function notifySysop(subject, body) {
+
+	var header = {
+		to : 'Jeopardized',
+		from : 'Jeopardized',
+		to_ext : 1,
+		from_ext : 1,
+		subject : subject
+	};
+
+	var msgBase = new MsgBase('mail');
+	msgBase.open();
+	msgBase.save_msg(header, body);
+	msgBase.close();
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/readme.txt b/xtrn/jeopardized/readme.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a513a3b6181dc99704b1c1c8c8a5fff0e5ceb17f
--- /dev/null
+++ b/xtrn/jeopardized/readme.txt
@@ -0,0 +1,224 @@
+Jeopardized!
+------------
+A networked trivia game for Synchronet BBS 3.15+
+by echicken, January 2016
+
+
+Contents
+--------
+
+	1) About
+	2) Installation
+		2.1) Copy Files
+		2.2) External Program Setup
+		2.3) Update
+	3) Hosting Your own Game
+		3.1) Hosting your own Jeopardized! JSON Database
+			3.1.1) Add a JSONDB module
+			3.1.2) Schedule a nightly maintenance event
+		3.2) Hosting your own Jeopardy! Web API
+	4) Support
+		4.1) DOVE-Net
+		4.2) electronic chicken bbs
+		4.3) IRC
+		4.4) Email
+
+
+1) About
+--------
+
+	Jeopardized! is a networked trivia game for Synchronet BBS inspired by (and
+	using data from) the Jeopardy! TV game show.  This game should work with
+	Synchronet BBS 3.15 and above.
+
+	Players on all participating BBSs are presented with the same categories and
+	questions, and can play up to three rounds of the game each day.  The game
+	is reset each night, however players' running totals and statistics will
+	persist.
+
+	More information is available in the in-game help screen.
+
+
+2) Installation
+---------------
+
+	2.1) Copy Files
+	---------------
+
+		Copy the 'jeopardized' directory that you extracted from the
+		installation archive into your 'xtrn/' directory, where 'xtrn/' is
+		relative to the root of your Synchronet BBS installation.		
+
+	2.2) External Program Setup
+	---------------------------
+
+		Add a new External Program, supplying these details at the prompts:
+
+			Online Program Name: Jeopardized!
+
+			Internal Code: JEOPARDY
+
+		Edit the program so that its settings look like the following:
+
+			Name						Jeopardized!
+			Internal Code				JEOPARDY
+			Start-up Directory			../xtrn/Jeopardized
+			Command Line				?jeopardized.js
+			Multiple Concurrent Users	Yes
+
+		Any other settings not mentioned above can be left alone.
+
+	2.3) Update
+	-----------
+
+		You can skip this step if you believe that your BBS is fairly up to
+		date.  However, if you run into problems, updating these files may help.
+
+		Grab the latest copies of the following files:
+
+			- exec/load/frame.js
+			- exec/load/tree.js
+			- exec/load/layout.js
+			- exec/load/event-timer.js
+			- exec/load/json-client.js
+			- exec/load/typeahead.js
+			- exec/load/scrollbar.js
+
+			All paths above are relative to the root of your Synchronet BBS
+			installation.
+
+			You can get these files by doing a CVS update of your 'exec/load/'
+			directory, by browsing the Synchronet CVS on the web here:
+
+				http://cvs.synchro.net/cgi-bin/viewcvs.cgi/exec/load/
+
+			or by downloading the nightly sbbs_run.zip archive from here:
+
+				http://vert.synchro.net/Synchronet/sbbs_run.zip
+
+
+3) Hosting Your own Game (or: Don't Do It)
+------------------------------------------
+
+	3.1) Hosting your own Jeopardized! JSON Database
+
+		The default 'settings.ini' file tells your local copy of this program to
+		connect to a game database server at 'bbs.electronicchicken.com'.  For
+		ease of setup, and to allow your users to play with everyone else using
+		that shared database, it's recommended that you leave these settings as
+		they are.
+
+		However, if you prefer to limit the game and its in-game chat features
+		to your own BBS, you can do the following:
+
+		3.1.1) Add a JSONDB module
+		--------------------------
+
+			Edit your 'ctrl/json-service.ini' file and add a section like this:
+
+				[jeopardized]
+				dir=../xtrn/jeopardized/server/
+
+			If you aren't already running the JSONDB service, then edit your
+			'ctrl/services.ini' file and add a section like this:
+
+				[JSONDB]
+				Port=10088
+				Options=STATIC | LOOP
+				Command=json-service.js
+
+			Recycle your 'Services' thread, or restart your BBS.
+
+			Edit your 'xtrn/jeopardized/settings.ini' file, and in the [JSONDB]
+			section change the 'host' value to point to your BBS ('localhost'
+			should do) and edit	the 'port' value if necessary.
+
+		3.1.2) Schedule a nightly maintenance event
+		-------------------------------------------
+
+			In SCFG, add a new Timed event as follows:
+
+				Internal Code					JPRDYMNT
+				Start-up directory 				../xtrn/jeopardized/server
+				Command Line					?maintenance.js
+				Enabled							Yes
+				Execution Days of Week			All
+				Execution Time 					00:00
+				Always Run After Init/Re-init	No
+
+			Leave any other settings alone.
+
+	3.2) Hosting your own Jeopardy! Web API
+	---------------------------------------
+
+		The nightly maintenance task fetches categories and questions from a
+		web API.  The user-facing game itself does not talk to this API.  Even
+		if you do wish to host your own JSONDB module for this game, you don't
+		necessarily need to host your own web API.  It's recommended that you
+		only follow this step if you have a good reason (eg. the author of this
+		program has died and the default web API is no longer online.)
+
+		The web API runs under node.js.  You'll want to start by installing it
+		from here:
+
+			https://nodejs.org/en/download/
+
+		You can grab a copy of the web API application here:
+
+			https://github.com/echicken/jeopardy-web-api
+
+		You must run 'npm install' in the application's directory in order to
+		install any dependencies.
+
+		This, in turn, relies on a 'clues.db' file generated by this:
+
+			https://github.com/whymarrh/jeopardy-parser
+
+		Which, in turn, relies on the J! Archive website.  If that disappears or
+		is offline, you're out of luck.
+
+		If you're successful in generating a 'clues.db' file, place it in the
+		top directory of the web API application.
+
+		If you manage to succeed in following all of the above steps, you can
+		run the web API by executing 'node index.js' in the application
+		directory.  Ideally you should run it as a service with something like
+		'forever' (https://www.npmjs.com/package/forever).
+
+		By default, the web API listens on port 3001.  You can change this by
+		editing the 'index.js' file.
+
+		To tell your maintenance script to connect to your own web API, edit the
+		[WebAPI] section of 'xtrn/jeopardized/settings.ini' and modify the URL
+		value as necessary.
+
+
+4) Support
+----------
+
+	You can contact me for support via any of the following means, in the
+	following order of preference:
+
+	DOVE-Net
+
+		Post a message to 'echicken' in the 'Synchronet Sysops' sub-board.
+		Unless I'm dead or on vacation, I'll probably get back to you within a
+		day or so.
+
+	electronic chicken bbs
+	
+		Post a message in the 'Support' sub-board of the 'Local' message group
+		on my BBS, bbs.electronicchicken.com
+
+	IRC : #synchronet on irc.synchro.net
+	
+		I'm not always in front of a computer, so you won't always receive an
+		immediate response if you contact me on IRC.  That said, if you stay
+		online and idle, I'll probably see your message and respond eventually.
+
+	Email
+	
+		You can email me at echicken -at- bbs.electronicchicken.com, however I
+		prefer to discuss problems & provide support in a public forum in case
+		any information that comes up can be of benefit to somebody else in the
+		future.
\ No newline at end of file
diff --git a/xtrn/jeopardized/server/commands.js b/xtrn/jeopardized/server/commands.js
new file mode 100644
index 0000000000000000000000000000000000000000..6df9f1ca78eb3fb3d46e2675ae7ac93cc54d25eb
--- /dev/null
+++ b/xtrn/jeopardized/server/commands.js
@@ -0,0 +1,58 @@
+var root = argv[0];
+
+this.QUERY = function(client, packet) {
+
+	var model = {
+		input : {
+			_permissions : [ 'SUBSCRIBE', 'UNSUBSCRIBE', 'READ', 'WRITE' ],
+			_types : [ 'object' ]
+		},
+		state : { 
+			_permissions : [ 'SUBSCRIBE', 'UNSUBSCRIBE', 'READ', 'WHO' ]
+		},
+		users : {
+			_permissions : [ 'SUBSCRIBE', 'UNSUBSCRIBE', 'READ' ],
+			_types : [ 'string' ]
+		},
+		game : { _permissions : [ 'SUBSCRIBE', 'UNSUBSCRIBE', 'READ' ] },
+		messages : { 
+			_permissions : [ 'SUBSCRIBE', 'UNSUBSCRIBE', 'READ', 'SLICE' ]
+		},
+		news : {
+			_permissions : [ 'SUBSCRIBE', 'UNSUBSCRIBE', 'READ', 'SLICE' ]
+		}
+	};
+
+	function allowed(packet) {
+		var location = packet.location.split('.')[0];
+		if (typeof model[location] === 'undefined' ||
+			typeof model[location]._permissions === 'undefined'
+		) {
+			return false;
+		}
+		if (model[location]._permissions.indexOf(packet.oper) >= 0 &&
+			(	(	packet.oper !== 'WRITE' &&
+					packet.oper !== 'SPLICE' &&
+					packet.oper !== 'PUSH' &&
+					packet.oper !== 'POP' &&
+					packet.oper !== 'SHIFT'
+				) ||
+				(	typeof model[location]._types === 'undefined' ||
+					model[location]._types.indexOf(typeof packet.data) >= 0
+				)
+			) &&
+			(	typeof model[location]._validate === 'undefined' ||
+				typeof model[location]._validate[typeof packet.data] !== 'function' ||
+				model[location]._validate[typeof packet.data](packet.data)
+			)
+		) {
+			return true;
+		}
+		return false;
+	}
+
+	if (allowed(packet) || admin.verify(client, packet, 90)) return false;
+
+	return true;
+
+}
diff --git a/xtrn/jeopardized/server/maintenance.js b/xtrn/jeopardized/server/maintenance.js
new file mode 100644
index 0000000000000000000000000000000000000000..6cabefe81d3f5ae54e4328c8ad2a9b9a69df64ef
--- /dev/null
+++ b/xtrn/jeopardized/server/maintenance.js
@@ -0,0 +1,259 @@
+load('sbbsdefs.js');
+load('http.js');
+load('json-client.js');
+
+var category_count = 5,
+	clue_count = 5;
+
+var settings = {},
+	jsonClient,
+	today = strftime('%d-%m-%Y');
+
+function getCategoryIDs() {
+	var IDs = JSON.parse(
+		(new HTTPRequest()).Get(settings.WebAPI.url + '/categories')
+	);
+	if (!Array.isArray(IDs) || IDs.length < 1) throw 'Invalid API response';
+	return IDs;
+}
+
+function getRandomCategory(IDs, round) {
+
+	var details;
+	var cc = round === 3 ? 1 : clue_count;
+
+	while (typeof details === 'undefined') {
+		var id = IDs[Math.floor(Math.random() * IDs.length)];
+		var d = JSON.parse(
+			(new HTTPRequest()).Get(settings.WebAPI.url + '/category/' + id)
+		);
+		if (d.clues[round - 1].length >= cc) {
+			details = d;
+			details.clues = [];
+		} else {
+			mswait(25);
+		}
+	}
+
+	var clues = JSON.parse(
+		(new HTTPRequest()).Get(
+			settings.WebAPI.url + '/category/' + id + '/' + round
+		)
+	).filter(
+		function (clue) {
+			return (clue.answer.search(/[^\x20-\x7f]/) < 0);
+		}
+	);
+
+	if (clues.length < cc) return;
+
+	var chosen = [];
+	while (details.clues.length < cc) {
+		var clue = Math.floor(Math.random() * clues.length);
+		if (chosen.indexOf(clue) < 0) {
+			chosen.push(clue);
+			details.clues.push(clues[clue]);
+			details.clues[details.clues.length - 1].value =
+				(details.clues.length * 100) * round;
+			details.clues[details.clues.length - 1].dd = false;
+		}
+	}
+
+	return details;
+
+}
+
+function getRound(IDs, round) {
+	var categories = [];
+	var cc = round === 3 ? 1 : category_count;
+	while (categories.length < cc) {
+		var category = getRandomCategory(IDs, round);
+		if (typeof category !== 'undefined') categories.push(category);
+	}
+	var ddc1 = Math.floor(Math.random() * cc);
+	var ddc2 = Math.floor(Math.random() * cc);
+	var ddcc1 = Math.floor(Math.random() * cc);
+	var ddcc2 = Math.floor(Math.random() * cc);
+	categories[ddc1].clues[ddcc1].dd = true;
+	categories[ddc2].clues[ddcc2].dd = true;
+	return categories;
+}
+
+function backupObj(obj, date, name) {
+	if (!file_isdir(js.exec_dir + 'backups')) mkdir(js.exec_dir + 'backups');
+	var f = new File(js.exec_dir + 'backups/' + date + '-' + name + '.json');
+	f.open('w');
+	f.write(JSON.stringify(obj));
+	f.close();
+}
+
+function backupYesterday() {
+
+	var state = jsonClient.read(settings.JSONDB.dbName, 'state', 1);
+	var game = jsonClient.read(settings.JSONDB.dbName, 'game', 1);
+	var users = jsonClient.read(settings.JSONDB.dbName, 'users', 1);
+	var messages = jsonClient.read(settings.JSONDB.dbName, 'messages', 1);
+	var news = jsonClient.read(settings.JSONDB.dbName, 'news', 1);
+
+	if (!state || !game || !users || !messages || !news) return true;
+
+	backupObj(game, state.date, 'game');
+	backupObj(users, state.date, 'users');
+	backupObj(messages, state.date, 'messages');
+	backupObj(news, state.date, 'news');
+
+	return (state.date !== today);
+
+}
+
+function processYesterday() {
+
+	var state = jsonClient.read(settings.JSONDB.dbName, 'state', 1);
+	var gameUsers = jsonClient.read(settings.JSONDB.dbName, 'game.users', 1);
+
+	if (!gameUsers) return;
+
+	var money = [];
+	var answers = [];
+
+	Object.keys(gameUsers).forEach(
+		function (key) {
+			money.push({ id : key, money : gameUsers[key].winnings });
+			answers.push(
+				{ id : key, answers : gameUsers[key].answers.correct }
+			);
+		}
+	);
+
+	money.sort(
+		function (a, b) {
+			return b.money - a.money;
+		}
+	);
+
+	answers.sort(
+		function (a, b) {
+			return b.answers - a.answers;
+		}
+	);
+
+	if (money.length > 3) money.splice(3, money.length - 3);
+
+	if (answers.length > 3) answers.splice(3, answers.length - 3);
+
+	if (money.length > 0 || answers.length > 0) {
+		var news = '\1h\1wStandings for \1c' + state.date + ':\r\n';
+	}
+
+	if (money.length > 0) {
+		news += '\t\1gWinnings:\r\n';
+		money.forEach(
+			function (e, i, a) {
+				var us = base64_decode(e.id).split('@');
+				news += '\t\t\1y#' + (i + 1) + ': ' +
+					' \1c' + us[0] + ' \1wwith \1g$' + e.money;
+				if (i !== a.length - 1) news += '\r\n';
+			}
+		);
+	}
+
+	if (answers.length > 0) {
+		news += '\r\n\t\1gCorrect answers:\r\n';
+		answers.forEach(
+			function (e, i, a) {
+				var us = base64_decode(e.id).split('@');
+				news += '\t\t\1y#' + (i + 1) + ': ' +
+					' \1c' + us[0] + ' \1wwith \1g' + e.answers;
+				if (i !== a.length - 1) news += '\r\n';
+			}
+		);
+	}
+
+	if (typeof news !== 'undefined') {
+		jsonClient.push(settings.JSONDB.dbName, 'news', news, 2);
+	}
+
+}
+
+function startNewDay() {
+
+	var state = {
+		locked : true,
+		date : today
+	};
+	jsonClient.write(settings.JSONDB.dbName, 'state', state, 2);
+
+	var IDs = getCategoryIDs();
+	var game = {
+		round : {
+			'1' : getRound(IDs, 1),
+			'2' : getRound(IDs, 2),
+			'3' : getRound(IDs, 3)
+		},
+		users : {}
+	};
+	jsonClient.write(settings.JSONDB.dbName, 'game', game, 2);	
+
+}
+
+function loadSettings() {
+	var f = new File(js.exec_dir + '../settings.ini');
+	f.open('r');
+	settings.JSONDB = f.iniGetObject('JSONDB');
+	settings.WebAPI = f.iniGetObject('WebAPI');
+	f.close();
+}
+
+function initJSON() {
+
+	var usr = new User(1);
+
+	jsonClient = new JSONClient(settings.JSONDB.host, settings.JSONDB.port);
+	jsonClient.ident('ADMIN', usr.alias, usr.security.password.toUpperCase());
+
+	var messages = jsonClient.read(settings.JSONDB.dbName, 'messages', 1);
+	if (!messages) {
+		jsonClient.write(
+			settings.JSONDB.dbName,
+			'messages',
+			{ latest : {}, history : [] },
+			2
+		);
+	} else if (messages.history.length > 50) {
+		var keep = messages.history.slice(-50);
+		messages.history = keep;
+		jsonClient.write(settings.JSONDB.dbName, 'messages', messages, 2);
+	}
+
+	var news = jsonClient.read(settings.JSONDB.dbName, 'news', 1);
+	if (!news) {
+		jsonClient.write(settings.JSONDB.dbName, 'news', [], 2);
+	} else if (news.length > 50) {
+		var keep = news.slice(-50);
+		news.history = keep;
+		jsonClient.write(settings.JSONDB.dbName, 'news', news, 2);
+	}
+
+	if (!jsonClient.read(settings.JSONDB.dbName, 'users', 1)) {
+		jsonClient.write(settings.JSONDB.dbName, 'users', {}, 2);
+	}
+
+	if (backupYesterday()) {
+		processYesterday();
+		startNewDay();
+	}
+
+}
+
+function cleanUp() {
+	jsonClient.write(settings.JSONDB.dbName, 'state.locked', false, 2);
+}
+
+try {
+	loadSettings();
+	initJSON();
+} catch (err) {
+	log(LOG_ERR, err);
+} finally {
+	cleanUp();
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/server/service.js b/xtrn/jeopardized/server/service.js
new file mode 100644
index 0000000000000000000000000000000000000000..4cae8483a7aefae61975a57358e9cd4231531b92
--- /dev/null
+++ b/xtrn/jeopardized/server/service.js
@@ -0,0 +1,348 @@
+js.branch_limit = 0;
+js.time_limit = 0;
+
+load('sbbsdefs.js');
+load('http.js');
+load('json-client.js');
+load(argv[0] + '../lib/func.js');
+
+var settings,
+	jsonClient;
+
+function validateUser(data) {
+
+	if (typeof data.id !== 'string' || data.id.length < 5) return false;
+
+	if (typeof data.alias !== 'string' ||
+		data.alias.length < 1 ||
+		data.alias.length > LEN_ALIAS
+	) {
+		return false;
+	}
+
+	if (typeof data.system !== 'string' ||
+		data.system.length < 1 ||
+		data.system.length > 50
+	) {
+		return false;
+	}
+
+	var u = jsonClient.read(settings.JSONDB.dbName, 'users.' + data.id, 1);
+	if (typeof u === 'object') return false;
+
+	return true;
+
+}
+
+function validateMessage(data) {
+	if ((	typeof data.alias === 'string' &&
+			(data.alias.length < 1 || data.alias.length > LEN_ALIAS)
+		) ||
+		(typeof data.alias !== 'undefined' && typeof data.alias !== 'string')
+	) {
+		return false;
+	}
+	if (typeof data.system !== 'string' ||
+		data.system.length < 1 ||
+		data.system.length > 50
+	) {
+		return false;
+	}
+	if (typeof data.message !== 'string' || data.message.length < 1) {
+		return false;
+	}
+	return true;
+}
+
+function processInput(update) {
+
+	if (typeof update.data !== 'object' ||
+		typeof update.data.key !== 'string' ||
+		typeof update.data.data !== 'object'
+	) {
+		return false;
+	}
+
+	var key = update.data.key;
+	var oper = update.data.oper;
+	var data = update.data.data;
+
+	switch (key) {
+
+		case 'users':
+			if (validateUser(data)) {
+				var id = data.id;
+				delete data.id;
+				data.created = time();
+				data.laston = time();
+				data.winnings = 0;
+				data.answers = { correct : 0, incorrect : 0 };
+				jsonClient.write(settings.JSONDB.dbName, 'users.'+id, data, 2);
+			} 
+			break;
+
+		case 'messages':
+			if (validateMessage(data)) {
+				data.created = time();
+				if (typeof data.store === 'boolean' && data.store) {
+					jsonClient.unshift(
+						settings.JSONDB.dbName,
+						'messages.history',
+						data,
+						2
+					);
+				}
+				jsonClient.write(
+					settings.JSONDB.dbName,
+					'messages.latest',
+					data,
+					2
+				);
+			}
+			break;
+
+		case 'game.users':
+			if (typeof data.id === 'string') {
+				var ugs = jsonClient.read(
+					settings.JSONDB.dbName,
+					'game.users.' + data.id,
+					1
+				);
+				if (typeof ugs === 'undefined') {
+					var obj = {
+						winnings : 0,
+						round : 1,
+						rounds : {
+							'1' : { '0':[], '1':[], '2':[], '3':[], '4':[] },
+							'2' : { '0':[], '1':[], '2':[], '3':[], '4':[] },
+							'3' : { '0':[] }
+						},
+						answers : { correct : 0, incorrect : 0 }
+					};
+					jsonClient.write(
+						settings.JSONDB.dbName,
+						'game.users.' + data.id,
+						obj,
+						2
+					);
+				}
+			}
+			break;
+
+		case 'game.state':
+			if (typeof data.id === 'string' &&
+				typeof data.round === 'number' &&
+				typeof data.category === 'number' &&
+				typeof data.clue === 'number' &&
+				data.round >= 1 && data.round <= 3 &&
+				data.category >= 0 && data.category < 5 &&
+				data.clue >= 0 && data.clue < 5
+			) {
+				jsonClient.push(
+					settings.JSONDB.dbName,
+					'game.users.' + data.id +
+						'.rounds.' + data.round + '.' + data.category,
+					data.clue,
+					2
+				);
+			}
+			break;
+
+		case 'game.nextRound':
+			if (typeof data.id === 'string') {
+				var ugs = jsonClient.read(
+					settings.JSONDB.dbName,
+					'game.users.' + data.id,
+					1
+				);
+				if (ugs && ugs.round <= 3 && ugs.winnings >= 0) {
+					ugs.round++;
+					jsonClient.write(
+						settings.JSONDB.dbName,
+						'game.users.' + data.id + '.round',
+						ugs.round,
+						2
+					);
+				}
+			}
+			break;
+
+		case 'game.report':
+			if (typeof data.userID === 'string' &&
+				typeof data.clueID === 'number' &&
+				typeof data.userAnswer === 'string' &&
+				typeof data.realAnswer === 'string'
+			) {
+				var us = base64_decode(data.userID).split('@');
+				if (us.length < 2) us.push('Unknown');
+				var body = format(
+					'User: %s\r\n' +
+					'System: %s\r\n' +
+					'Clue ID: %s\r\n' +
+					'Given answer: %s\r\n' +
+					'Correct answer: %s\r\n' +
+					'\r\nThis message was generated by Jeopardized\r\n',
+					us[0], us[1], data.clueID, data.userAnswer, data.realAnswer
+				);
+				notifySysop('Jeopardized clue ' + data.clueID, body);
+			}
+			break;
+
+		case 'game.answer':
+			if (typeof data.id === 'string' &&
+				typeof data.round === 'number' &&
+				typeof data.category === 'number' &&
+				typeof data.clue === 'number' &&
+				typeof data.answer === 'string'
+			) {
+				var category = jsonClient.read(
+					settings.JSONDB.dbName,
+					'game.round.' + data.round + '.' + data.category,
+					1
+				);
+				var clue = category.clues[data.clue];
+				var ugs = jsonClient.read(
+					settings.JSONDB.dbName,
+					'game.users.' + data.id,
+					1
+				);
+				var usr = jsonClient.read(
+					settings.JSONDB.dbName,
+					'users.' + data.id,
+					1
+				);
+				if (typeof category === 'undefined' ||
+					typeof ugs === 'undefined' ||
+					typeof usr === 'undefined'
+				) {
+					return;
+				}
+				var value = (
+					clue.dd && data.value !== null
+					? data.value
+					: clue.value
+				);
+				try {
+					var success = compareAnswer(clue.id, data.answer);
+					if (success) {
+						ugs.winnings = ugs.winnings + value;
+						usr.winnings = usr.winnings + value;
+						ugs.answers.correct++;
+						usr.answers.correct++;
+					} else {
+						ugs.winnings = ugs.winnings - value;
+						usr.winnings = usr.winnings - value;
+						ugs.answers.incorrect++;
+						usr.answers.incorrect++;
+					}
+					jsonClient.write(
+						settings.JSONDB.dbName,
+						'game.users.' + data.id,
+						ugs,
+						2
+					);
+					jsonClient.write(
+						settings.JSONDB.dbName,
+						'users.' + data.id,
+						usr,
+						2
+					);
+					if (clue.dd && data.value !== null) {
+						var msg = '%s%s@%s bet $%s and %s!';
+						if (success) {
+							msg = format(
+								msg,
+								'\1h\1g',
+								usr.alias,
+								usr.system,
+								data.value,
+								'won'
+							);
+						} else {
+							msg = format(
+								msg,
+								'\1h\1r',
+								usr.alias,
+								usr.system,
+								data.value,
+								'lost'
+							);
+						}
+						jsonClient.unshift(
+							settings.JSONDB.dbName,
+							'messages.history',
+							{ message : msg },
+							2
+						);
+						jsonClient.write(
+							settings.JSONDB.dbName,
+							'messages.latest',
+							{ message : msg },
+							2
+						);
+					}
+				} catch (err) {
+					notifySysop('Web API error', err);
+				}
+			}
+			break;
+
+		default:
+			break;
+
+	}
+
+}
+
+function processUpdate(update) {
+
+	if (typeof update.location === 'undefined' ||
+		typeof update.oper !== 'string' ||
+		update.oper !== 'WRITE'
+	) {
+		return;
+	}
+
+	if (update.location === 'input') {
+		processInput(update);
+	}
+
+}
+
+function loadSettings() {
+	settings = {};
+	var f = new File(argv[0] + '../settings.ini');
+	f.open('r');
+	settings.JSONDB = f.iniGetObject('JSONDB');
+	settings.WebAPI = f.iniGetObject('WebAPI');
+	f.close();
+}
+
+function initJSON() {
+	var usr = new User(1);
+	jsonClient = new JSONClient(settings.JSONDB.host, settings.JSONDB.port);
+	jsonClient.ident('ADMIN', usr.alias, usr.security.password);
+	jsonClient.callback = processUpdate;
+	jsonClient.subscribe(settings.JSONDB.dbName, 'input');
+}
+
+function main() {
+	while (!js.terminated) {
+		jsonClient.cycle();
+		mswait(5);
+	}
+}
+
+function cleanUp() {
+	jsonClient.disconnect();
+}
+
+try {
+	loadSettings();
+	initJSON();
+	main();
+} catch (err) {
+	notifySysop('Service thread encountered an error', err);
+} finally {
+	cleanUp();
+}
diff --git a/xtrn/jeopardized/settings.ini b/xtrn/jeopardized/settings.ini
new file mode 100644
index 0000000000000000000000000000000000000000..092562809df33a7563c51307c2879e57a935de9c
--- /dev/null
+++ b/xtrn/jeopardized/settings.ini
@@ -0,0 +1,9 @@
+[JSONDB]
+host = bbs.electronicchicken.com
+port = 10088
+dbName = JEOPARDIZED
+retries = 10
+retryDelay = 100
+
+[WebAPI]
+url = http://bbs.electronicchicken.com:3001
diff --git a/xtrn/jeopardized/views/answer.js b/xtrn/jeopardized/views/answer.js
new file mode 100644
index 0000000000000000000000000000000000000000..e39eed68e749f03626a11a3ed95d958e356cf10f
--- /dev/null
+++ b/xtrn/jeopardized/views/answer.js
@@ -0,0 +1,78 @@
+var Answer = function (frame, round, category, clue, answer, game) {
+
+	var frames = {
+		border : null,
+		top : null
+	};
+
+	game.state.game.rounds[round.number][category].push(clue);
+
+	function initDisplay() {
+		frames.border = new Frame(
+			frame.x + 5,
+			frame.y + 5,
+			70,
+			9,
+			WHITE,
+			frame
+		);
+		frames.top = new Frame(1, 1, 1, 1, BG_BLUE|WHITE, frames.border);
+		frames.top.nest(1, 1);
+		frames.border.drawBorder([LIGHTBLUE,CYAN,LIGHTCYAN,WHITE]);
+		frames.border.open();
+	}
+
+	function getResult() {
+		value = (
+			game.state.wager === null
+			? round[category].clues[clue].value
+			: game.state.wager
+		);
+		if (compareAnswer(round[category].clues[clue].id, answer)) {
+			frames.top.attr = GREEN;
+			frames.top.center('Correct!');
+			game.winnings = value;
+		} else {
+			frames.top.attr = RED;
+			frames.top.center('Incorrect!');
+			frames.top.putmsg('\r\n\r\n');
+			frames.top.attr = LIGHTGRAY;
+			frames.top.center('The correct answer is:');
+			frames.top.crlf();
+			frames.top.attr = WHITE;
+			frames.top.center(round[category].clues[clue].answer);
+			game.winnings = (0 - value);
+		}
+		frames.top.putmsg('\r\n\r\n');
+		frames.top.attr = LIGHTBLUE;
+		frames.top.center('[Press any key to continue]\r\n');
+		frames.top.center('[Press \1h\1rR\1h\1b to report a bad clue]');
+	}
+
+	function init() {
+		initDisplay();
+		getResult();
+	}
+
+	this.getcmd = function (cmd) {
+		if (cmd.toUpperCase() === 'R') {
+			frames.top.gotoxy(1, frames.top.height);
+			frames.top.clearline();
+			frames.top.gotoxy(1, frames.top.height);
+			frames.top.center('\1h\1rThis clue has been flagged.');
+			database.reportClue(user, round[category].clues[clue], answer);
+			return false;
+		} else {
+			return (cmd !== '');
+		}
+	}
+
+	this.cycle = function () { }
+
+	this.close = function () {
+		frames.border.close();
+	}
+
+	init();
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/bin-scroller.js b/xtrn/jeopardized/views/bin-scroller.js
new file mode 100644
index 0000000000000000000000000000000000000000..eea36c507c5c48dee49643ba6cbb7d0d5cd6c8b7
--- /dev/null
+++ b/xtrn/jeopardized/views/bin-scroller.js
@@ -0,0 +1,52 @@
+var BinScroller = function (frame, bin, w, h) {
+
+	var frames = {
+		border : null,
+		bin : null
+	};
+
+	var scrollbar;
+
+	function init() {
+		frames.border = new Frame(1, 1, 1, 1, WHITE, frame);
+		frames.border.nest();
+		frames.border.drawBorder([LIGHTBLUE,CYAN,LIGHTCYAN,WHITE]);
+		frames.bin = new Frame(1, 1, 1, 1, WHITE, frames.border);
+		frames.bin.nest(1, 1);
+		frames.bin.load(bin, w, h);
+		scrollbar = new ScrollBar(frames.bin);
+		frames.border.open();
+	}
+
+	this.cycle = function () {
+		scrollbar.cycle();
+	}
+
+	this.getcmd = function (cmd) {
+		var ret = true;
+		switch (cmd) {
+			case KEY_UP:
+				frames.bin.scroll(0, -1);
+				break;
+			case KEY_DOWN:
+				if (frames.bin.data_height > frames.bin.height &&
+					frames.bin.offset.y + frames.bin.height <
+						frames.bin.data_height
+				) {
+					frames.bin.scroll(0, 1);
+				}
+				break;
+			default:
+				if (cmd !== '') ret = false;
+				break;
+		}
+		return ret;
+	}
+
+	this.close = function () {
+		frames.border.close();
+	}
+
+	init();
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/board.js b/xtrn/jeopardized/views/board.js
new file mode 100644
index 0000000000000000000000000000000000000000..e6f22d4bbca1b0ffeb5760423ef70305b25362bb
--- /dev/null
+++ b/xtrn/jeopardized/views/board.js
@@ -0,0 +1,233 @@
+var Board = function(frame, round, ugs) {
+
+	var COLUMN_WIDTH = 14;
+	var HEADER_HEIGHT = 5;
+
+	var state = {
+		column : 0,
+		row : 0
+	};
+
+	var frames = {
+		top : null,
+		category : {
+			'0' : {
+				header : null,
+				clues : {
+					'0' : null,
+					'1' : null,
+					'2' : null,
+					'3' : null,
+					'4' : null
+				}
+			},
+			'1' : {
+				header : null,
+				clues : {
+					'0' : null,
+					'1' : null,
+					'2' : null,
+					'3' : null,
+					'4' : null
+				}
+			},
+			'2' : {
+				header : null,
+				clues : {
+					'0' : null,
+					'1' : null,
+					'2' : null,
+					'3' : null,
+					'4' : null
+				}
+			},
+			'3' : {
+				header : null,
+				clues : {
+					'0' : null,
+					'1' : null,
+					'2' : null,
+					'3' : null,
+					'4' : null
+				}
+			},
+			'4' : {
+				header : null,
+				clues : {
+					'0' : null,
+					'1' : null,
+					'2' : null,
+					'3' : null,
+					'4' : null
+				}
+			}
+		}
+	};
+
+	function highlight(frame, bool) {
+		bgAttr = (
+			bool
+			? BG_CYAN
+			: (frame._jclueval === '' ? BG_LIGHTGRAY : BG_BLUE)
+		);
+		frame.attr = bgAttr|WHITE;
+		frame.clear();
+		frame.center(frame._jclueval);
+	}
+
+	function navigate(direction) {
+		switch (direction) {
+			case KEY_UP:
+				if (state.row > 0) {
+					highlight(
+						frames.category[state.column].clues[state.row],
+						false
+					);
+					state.row--;
+					highlight(
+						frames.category[state.column].clues[state.row],
+						true
+					);
+				}
+				break;
+			case KEY_DOWN:
+				if (state.row < 4) {
+					highlight(
+						frames.category[state.column].clues[state.row],
+						false
+					);
+					state.row++;
+					highlight(
+						frames.category[state.column].clues[state.row],
+						true
+					);
+				}
+				break;
+			case KEY_LEFT:
+				if (state.column > 0) {
+					highlight(
+						frames.category[state.column].clues[state.row],
+						false
+					);
+					state.column--;
+					highlight(
+						frames.category[state.column].clues[state.row],
+						true
+					);
+				}
+				break;
+			case KEY_RIGHT:
+				if (state.column < 4) {
+					highlight(
+						frames.category[state.column].clues[state.row],
+						false
+					);
+					state.column++;
+					highlight(
+						frames.category[state.column].clues[state.row],
+						true
+					);
+				}
+				break;
+			default:
+				break;
+		}
+	}
+
+	function drawCategory(category, index) {
+
+		frames.category[index].header = new Frame(
+			frames.top.x + (index * (COLUMN_WIDTH + 2)),
+			frames.top.y,
+			COLUMN_WIDTH,
+			HEADER_HEIGHT,
+			BG_BLUE|WHITE,
+			frames.top
+		);
+		frames.category[index].header.word_wrap = true;
+		frames.category[index].header.centerWrap(category.name);
+		if (frames.category[index].header.data_height >
+				frames.category[index].header.height
+		) {
+			frames.category[index].header.scroll(0, -1);			
+		}
+
+		category.clues.forEach(
+			function (clue, cIndex) {
+				frames.category[index].clues[cIndex] = new Frame(
+					frames.category[index].header.x,
+					(	frames.category[index].header.y + 
+						frames.category[index].header.height + 1 +
+						(cIndex * 2)
+					),
+					COLUMN_WIDTH,
+					1,
+					BG_BLUE|WHITE,
+					frames.top
+				);
+				if (ugs.rounds[round.number][index].indexOf(cIndex) >= 0) {
+					frames.category[index].clues[cIndex]._jclueval = '';
+					highlight(frames.category[index].clues[cIndex], false);
+				} else {
+					var value = clue.value;
+					if (value > 999) {
+						value += '';
+						value = value.split('');
+						value.splice(1, 0, ',');
+						value = value.join('');
+					}
+					value = '$' + value;
+					frames.category[index].clues[cIndex].center(value);
+					frames.category[index].clues[cIndex]._jclueval = value;
+				}
+			}
+		);
+
+	}
+
+	function initDisplay() {
+		frames.top = new Frame(
+			frame.x + 1,
+			frame.y,
+			(COLUMN_WIDTH * round.length) + round.length + 2,
+			HEADER_HEIGHT + (round[0].clues.length * 2),
+			WHITE,
+			frame
+		);
+		round.forEach(drawCategory);
+		frames.top.open();
+		highlight(frames.category['0'].clues['0'], true);
+	}
+
+	this.getcmd = function (cmd) {
+		var ret;
+		switch (cmd) {
+			case KEY_UP:
+			case KEY_DOWN:
+			case KEY_LEFT:
+			case KEY_RIGHT:
+				navigate(cmd);
+				break;
+			case '\r':
+				var c = frames.category[state.column].clues[state.row];
+				if (c._jclueval !== '') {
+					c.clear();
+					c._jclueval = '';
+					ret = state;
+				}
+				break;
+			default:
+				break;
+		}
+		return ret;
+	}
+
+	this.close = function () {
+		frames.top.close();
+	}
+
+	this.cycle = function () { }
+
+	initDisplay();
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/clue.js b/xtrn/jeopardized/views/clue.js
new file mode 100644
index 0000000000000000000000000000000000000000..82929094220fd0d8d77632dc058c99256a355f08
--- /dev/null
+++ b/xtrn/jeopardized/views/clue.js
@@ -0,0 +1,147 @@
+var Clue = function(frame, round, category, c) {
+
+	var self = this;
+
+	var state = STATE_CLUE_INPUT;
+
+	var TIME_LIMIT = 30;
+	var ticks = TIME_LIMIT;
+
+	var typeahead,
+		timer;
+
+	var clue = round[category].clues[c];
+
+	this.expired = false;
+	this.category = category;
+	this.clue = c;
+
+	var frames = {
+		border: null,
+		top : null,
+		clue : null,
+		hint : null
+	};
+
+	function initDisplay() {
+
+		frames.border = new Frame(
+			frame.x + 1,
+			frame.y + 5,
+			frame.width - 2,
+			14,
+			WHITE,
+			frame
+		);
+		frames.border.checkbounds = false;
+
+		frames.top = new Frame(1, 1, 1, 1, WHITE, frames.border);
+		frames.top.nest(1, 1);
+		frames.top.checkbounds = false;
+		frames.top.center(round[category].name + ' for ' + clue.value);
+
+		frames.clue = new Frame(
+			frames.top.x + 1,
+			frames.top.y + 1,
+			frames.top.width - 2,
+			frames.top.height - 5,
+			BG_BLUE|WHITE,
+			frames.top
+		);
+		frames.clue.word_wrap = true;
+		frames.clue.putmsg(clue.clue);
+
+		frames.time = new Frame(
+			frames.top.x + 1,
+			frames.clue.y + frames.clue.height + 2,
+			frames.clue.width,
+			1,
+			GREEN,
+			frames.top
+		);
+		frames.time.putmsg('\1h\1wTime left: \1n\1g' + TIME_LIMIT);
+
+		frames.hint = new Frame(
+			frames.time.x,
+			frames.time.y + 1,
+			frames.clue.width,
+			1,
+			WHITE,
+			frames.top
+		);
+		var hint = clue.answer.replace(/\s/g, '  ');
+		hint = hint.replace(/\w/g, '_ ');
+		frames.hint.putmsg('Hint: ' + hint);
+
+		frames.border.drawBorder([LIGHTBLUE, CYAN, LIGHTCYAN, WHITE]);
+		frames.border.open();
+
+		typeahead = new Typeahead(
+			{	x : frames.top.x + 1,
+				y : frames.clue.y + frames.clue.height + 1,
+				frame : frames.top,
+				len : frames.top.width - 2,
+				fg : WHITE
+			}
+		);
+
+	}
+
+	function initTimer() {
+
+		timer = new Timer();
+
+		timer.addEvent(
+			TIME_LIMIT * 1000,
+			false,
+			function () {
+				self.expired = true;
+			}
+		);
+
+		timer.addEvent(
+			1000,
+			TIME_LIMIT,
+			function () {
+				ticks--;
+				if (ticks < 10) {
+					frames.time.attr = RED;
+				} else if (ticks < 20) {
+					frames.time.attr = BROWN;
+				}
+				frames.time.gotoxy(12, 1);
+				frames.time.putmsg(ticks + ' ');
+			}
+		);
+
+	}
+
+	function init() {
+		initDisplay();
+		initTimer();
+	}
+
+	this.cycle = function () {
+		typeahead.cycle();
+		timer.cycle();
+	}
+
+	this.close = function () {
+		typeahead.close();
+		frames.border.close();
+	}
+
+	this.getcmd = function (cmd) {
+		var ret = typeahead.inkey(cmd);
+		switch (state) {
+			case STATE_CLUE_INPUT:
+				break;
+			default:
+				break;
+		}
+		return ret;
+	}
+
+	init();
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/credits.bin b/xtrn/jeopardized/views/credits.bin
new file mode 100644
index 0000000000000000000000000000000000000000..31aab7a842831dbef8db7fa4e4416141b870df43
--- /dev/null
+++ b/xtrn/jeopardized/views/credits.bin
@@ -0,0 +1 @@
+Jeopardized! Credits:                          (UP/DOWN to scroll, Q to quit)ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ                       This is Jeopardized! by echicken                                               [	bbs.electronicchicken.com]	                                                               ÄÄÄ                                                   Questions and answers extracted from J! ARCHIVE                                          [	http://j-archive.com/]	                                                                 ÄÄÄ                                            J! ARCHIVE data scraping by Whymarrh Whitby's jeopardy-parser                       [	https://github.com/whymarrh/jeopardy-parser/]	                                                      ÄÄÄ                                    Jeopardy! game show, format, questions & answers by Jeopardy Productions Inc.                          [	http://www.jeopardy.com/]	                                                               ÄÄÄ                                                  Several supporting Javascript libraries by MCMLXXIX                                       [	bbs.thebrokenbubble.com]	                                                               ÄÄÄ                                                       And none of this would be possible without                              Synchronet BBS [	http://synchro.net/]	 by Digital Man,                             with many contributions by Deuce and others                
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/game.js b/xtrn/jeopardized/views/game.js
new file mode 100644
index 0000000000000000000000000000000000000000..40d5980d7b1e67a1387a25c61e3c2741186cdf48
--- /dev/null
+++ b/xtrn/jeopardized/views/game.js
@@ -0,0 +1,233 @@
+var Game = function (frame) {
+
+	var self = this;
+
+	var popUp,
+		messages;
+
+	this.state = {
+		usr : null,
+		game : null,
+		round : null,
+		wager : null,
+		state : STATE_GAME_PLAY
+	};
+
+	var frames = {
+		border : null,
+		top : null,
+		board : null,
+		stats : null
+	};
+
+	function initData() {
+
+		self.state.usr = database.getUser(user);
+		if (typeof self.state.usr === 'undefined') {
+			throw 'DB: error reading users.' + database.getUserID(user);
+		}
+
+		self.state.game = database.getUserGameState(user);
+		if (typeof self.state.game === 'undefined') {
+			throw 'DB: error reading game.users.' + database.getUserID(user);
+		}
+
+	}
+
+	function initDisplay() {
+
+		frames.top = new Frame(
+			frame.x,
+			frame.y,
+			frame.width,
+			frame.height,
+			WHITE,
+			frame
+		);
+		frames.top.checkbounds = false;
+
+		frames.board = new Frame(
+			frames.top.x,
+			frames.top.y,
+			frames.top.width,
+			16,
+			WHITE,
+			frames.top
+		);
+
+		frames.stats = new Frame(
+			frames.top.x,
+			frames.board.y + frames.board.height,
+			frames.top.width,
+			1,
+			BG_BLUE|WHITE,
+			frames.top
+		);
+
+		frames.top.open();
+
+	}
+
+	function startRound() {
+		if (self.state.game.round > 0 && self.state.game.round < 4 &&
+			(	(	self.state.game.round < 3 &&
+					countAnswers(self.state.game, self.state.game.round) < 25
+				) ||
+				(	self.state.game.round === 3 &&
+					countAnswers(self.state.game, self.state.game.round) < 1
+				)
+			)
+		) {
+			self.state.round = new Round(
+				frames.board,
+				self.state.game.round,
+				self
+			);
+			popUp = new PopUp(
+				frame,
+				'Starting round ' + self.state.game.round,
+				'[Press any key to continue]',
+				PROMPT_ANY,
+				STATE_GAME_PLAY
+			);
+		} else {
+			popUp = new PopUp(
+				frame,
+				"You've gone as far as you can for today.\r\n" +
+				"Please come back tomorrow to play again.",
+				"[Press any key to continue]",
+				PROMPT_ANY
+			);
+		}
+		refreshStats();
+		self.state.state = STATE_GAME_POPUP;
+	}
+
+	function refreshStats() {
+		frames.stats.clear();
+		frames.stats.center(
+			format(
+				'Round: \1h\1c%s\1h\1w  ' +
+				'Today: \1h\1%s$%s\1h\1w  ' +
+				'Total: \1h\1%s$%s\1h\1w  ' +
+				'Answers: \1h\1g%s\1h\1w:\1h\1r%s ',
+				self.state.game.round,
+				self.state.game.winnings > 0 ? 'g' : 'r',
+				self.state.game.winnings,
+				self.state.usr.winnings > 0 ? 'g' : 'r',
+				self.state.usr.winnings,
+				self.state.usr.answers.correct,
+				self.state.usr.answers.incorrect
+			)
+		);
+		frames.stats.gotoxy(frames.stats.width - 10, 1);
+		frames.stats.putmsg('\1h\1wQ\1cuit');
+	}
+
+	function init() {
+		initData();
+		initDisplay();
+		messages = new Messages(
+			frames.top,
+			frames.top.x,
+			frames.stats.y + 1,
+			frames.top.width,
+			7,
+			true
+		);
+		startRound();
+	}
+
+	this.getcmd = function (cmd) {
+		var ret = true;
+		switch (this.state.state) {
+			case STATE_GAME_PLAY:
+				if (cmd === '\x09') {
+					messages.focus = true;
+					this.state.state = STATE_GAME_MESSAGES;
+				} else {
+					ret = this.state.round.getcmd(cmd);
+					if (ret === RET_ROUND_QUIT) {
+						this.state.round.close();
+						ret = false;
+					} else if (ret === RET_ROUND_NEXT) {
+						this.state.round.close();
+						database.nextRound(user);
+						this.state.game.round++;
+						startRound();
+						ret = true;
+					} else if (ret === RET_ROUND_STOP) {
+						this.state.round.close();
+						this.state.game.round = -1;
+						startRound();
+						ret = true;
+					} else if (ret === RET_ROUND_LAST) {
+						this.state.round.close();
+						database.nextRound(user);
+						this.state.game.round = 4;
+						startRound();
+						ret = true;
+					} else {
+						ret = true;
+					}
+				}
+				break;
+			case STATE_GAME_MESSAGES:
+				if (cmd === '\x09') {
+					messages.focus = false;
+					this.state.state = STATE_GAME_PLAY;
+				} else {
+					messages.getcmd(cmd);
+				}
+				break;
+			case STATE_GAME_POPUP:
+				if (typeof popUp.getcmd(cmd) !== 'undefined') {
+					var s = popUp.close();
+					if (typeof s !== 'undefined') {
+						this.state.state = s;
+					} else {
+						ret = false;
+					}
+				}
+				break;
+			default:
+				break;
+		}
+		return ret;
+	}
+
+	this.cycle = function () {
+		if (self.state.round !== null) self.state.round.cycle();
+		messages.cycle();
+	}
+
+	this.close = function () {
+		messages.close();
+		if (self.state.round !== null) self.state.round.close();
+		frames.top.close();
+	}
+
+	this.__defineGetter__(
+		'winnings',
+		function () {
+			return self.state.game.winnings;
+		}
+	);
+
+	this.__defineSetter__(
+		'winnings',
+		function (val) {
+			self.state.game.winnings = self.state.game.winnings + val;
+			self.state.usr.winnings = self.state.usr.winnings + val;
+			if (val > 0) {
+				self.state.usr.answers.correct++;
+			} else {
+				self.state.usr.answers.incorrect++;
+			}
+			refreshStats();
+		}
+	);
+
+	init();
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/help.bin b/xtrn/jeopardized/views/help.bin
new file mode 100644
index 0000000000000000000000000000000000000000..53caa699eae961a8d8618a06fc2ad7ec54d234e7
--- /dev/null
+++ b/xtrn/jeopardized/views/help.bin
@@ -0,0 +1 @@
+Jeopardized! Halp!                             (UP/DOWN to scroll, Q to quit)ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ      Messages        On the Main Menu or in the Game view,   Notifications  Ú	Ä	Ä	¿	Ú	Ä	¿	       Ú	Ä	Ä	¿	Ú	Ä	¿	 hit [TAB] to switch to the Messages   þþ	þ	þ	þ	       þ	þ	þ	þ	þ	À	Ä	Ä	Ù	À	Ä	Ù	 [TAB] À	Ä	Ä	Ù	À	Ä	Ù	 box.  Once the Messages box has been  þ	þ	þ	þ	þ	 [TAB] þ	þ	þ	þ	þ	Ú¿ÚÄÄÄ¿       Ú¿ÚÄÄÄ¿ highlighted, you can scroll by using  ÚÄÄÄ¿       ÚÄÄÄ¿ÀÙÀÄÄÄÙ       ÀÙÀÄÄÄÙ your arrow keys.                      ÀÄÄÄÙ       ÀÄÄÄÙ                                      ÄÄÄ                                    You can use the Messages box to chat with other players. If nobody is online at the time, someone will see your messages the next time they play.                                               ÄÄÄ                                    Game notifications and alerts from the BBS will also show up in the Messages box.  Keep an eye on this space to be notified of other players' progress in the game, or to be informed of goings-on outside of the game.                ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄGAMEþ	   Rounds 1 & 2 are made up of 5 categories of 5 clues each. To progressþ	PLAY   to the next round,  you must get at least 10 correct answers and haveÚÄÄÄ¿   at least $0 in winnings.                                             ÀÄÄÄÙ                                 ÄÄÄ                                    There are 2 Double Jeopardization clues hidden in each of these rounds.  If  you find one, you will be asked to wager between $5 and the maximum prize    amount for that round. Depending on your answer, you will win or lose this   amount of fake money.                                                                                              ÄÄÄ                                    Round 3 contains one clue.  You will be informed of the category that   ÚÄÄÄ¿this clue belongs to,  then be asked to wager anything from $0 up to    ÀÄÄÄÙyour total winnings for the day.                                        ÚÄÄÄ¿                                      ÄÄÄ                               ÀÄÄÄÙAs you play, money is added to and deducted from your daily and running      totals.  Your daily total affects what you can wager today and whether you   can progress to the next round.  Your running total represents your overall  standing in the current tournament.                                                                                ÄÄÄ                                    You do not need to phrase your responses in the form of a question.                                                ÄÄÄ                                    Users on all participating BBSs are presented with the same categories and   clues each day, and new game boards are generated every night.                                                     ÄÄÄ                                    If you are playing or attempt to enter the game during nightly maintenance,  the game will exit and you will be returned to the BBS.                                                            ÄÄÄ                                    You can exit a game in progress at any time, then return later the same day  to continue where you left off.                                              ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÚÄÄÄÄÄÄ¿                                                                     ³REPORT³ The questions and answers in this game were pulled from a website   ÀÄÄÄÄÄÄÙ maintained by fans of the Jeopardy! game show. As such, the format  of the data isn't always consistent. While steps have been taken to compare  your answers with those in the database as flexibly as possible, errors are  bound to occur. If you answer a question correctly but aren't given credit,  hit R while still in the Answer view to report the problem. This will send   an email to game admin, prompting them to look into the issue. Please use    this feature! You will be helping to improve the game for everyone.          
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/jeopardized.bin b/xtrn/jeopardized/views/jeopardized.bin
new file mode 100644
index 0000000000000000000000000000000000000000..ece499a449af4391a2a1124e8b4f8fab39421509
--- /dev/null
+++ b/xtrn/jeopardized/views/jeopardized.bin
@@ -0,0 +1 @@
+Ú	Ä	Ä	Ä	Ä	Ä	Ä	Ä	Ä	Ä	Ä	Ä	ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ Ú	Ä	Ä	Ä	Ä	Ä	Ä	Ä	ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿³	                                              ³ ³	ÛÛÛÛÛ ÛÛÛÛÛ ÛÛÛÛÛ ÛÛÛÛÛ ÛÛÛÛÛ³³	                 Jeopardized!                	 ³ ³	ÛÛÛÛÛ ÛÛÛÛÛ ÛÛÛÛÛ ÛÛÛÛÛ ÛÛÛÛÛ³³	        A daily inter-BBS trivia game.        	³ ³	ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜܳ³	                 B	y	: 	e	c	h	i	c	k	e	n	                 ³ ³	ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜܳ³	                                              ³ ³	ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜܳ³	  290,000 questions, 	infinite wrong answers.  ³ ³	ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜܳ³	                                              ³ ³	ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜÜ ÜÜÜÜܳÀ	Ä	Ä	Ä	Ä	Ä	Ä	Ä	Ä	Ä	Ä	Ä	ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ À	Ä	Ä	Ä	Ä	Ä	Ä	Ä	ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/menu.js b/xtrn/jeopardized/views/menu.js
new file mode 100644
index 0000000000000000000000000000000000000000..5e67181afdf400abcc61fdb50fe867e09799fda8
--- /dev/null
+++ b/xtrn/jeopardized/views/menu.js
@@ -0,0 +1,115 @@
+var Menu = function (frame) {
+
+	var self = this;
+	var state = STATE_MENU_TREE;
+
+	var frames = {
+		top : null,
+		menu : null,
+		tree : null
+	};
+
+	var tree,
+		messages,
+		title;
+
+	function initDisplay() {
+		frames.top = new Frame(1, 1, frame.width, frame.height, WHITE, frame);
+		frames.top.checkbounds = false;
+		frames.top.centralize(frame);
+		frames.menu = new Frame(
+			frames.top.x,
+			frames.top.y + 9,
+			12,
+			15,
+			WHITE,
+			frames.top
+		);
+		frames.tree = new Frame(1, 1, 1, 1, WHITE, frames.menu);
+		frames.tree.nest(1, 2);
+		title = {
+			x : frames.menu.width - 6,
+			y : 1,
+			attr : WHITE,
+			text : 'Menu'
+		};
+		frames.menu.drawBorder([LIGHTBLUE,CYAN,LIGHTCYAN,WHITE], title);
+		frames.top.open();
+		frames.top.load(js.exec_dir + 'views/jeopardized.bin', 80, 9);
+	}
+
+	function initTree() {
+		tree = new Tree(frames.tree);
+		tree.colors.fg = WHITE;
+		tree.colors.bg = BG_BLACK;
+		tree.colors.lfg = WHITE;
+		tree.colors.lbg = BG_BLUE;
+		tree.colors.kfg = LIGHTCYAN;
+		tree.addItem('|Play', STATE_GAME);
+		tree.addItem('|Scores', STATE_SCORE);
+		tree.addItem('|News', STATE_NEWS);
+		tree.addItem('|Help', STATE_HELP);
+		tree.addItem('|Credits', STATE_CREDIT);
+		tree.addItem('|Quit', STATE_QUIT);
+		tree.open();
+	}
+
+	function init() {
+		initDisplay();
+		initTree();
+		messages = new Messages(
+			frames.top,
+			frames.top.x + 13,
+			frames.top.y + 9,
+			frames.top.width - 13,
+			15,
+			true
+		);
+	}
+
+	this.cycle = function () {
+		tree.cycle();
+		messages.cycle();
+	}
+
+	this.getcmd = function (cmd) {
+		var ret = false;
+		switch (state) {
+			case STATE_MENU_TREE:
+				if (cmd === '\x09' || cmd === KEY_RIGHT) {
+					state = STATE_MENU_MESSAGES;
+					title.attr = LIGHTGRAY;
+					frames.menu.drawBorder(LIGHTGRAY, title);
+					messages.focus = true;
+				} else {
+					ret = tree.getcmd(cmd);
+				}
+				break;
+			case STATE_MENU_MESSAGES:
+				if (cmd === '\x09' || cmd === KEY_LEFT) {
+					state = STATE_MENU_TREE;
+					title.attr = WHITE;
+					frames.menu.drawBorder(
+						[LIGHTBLUE,CYAN,LIGHTCYAN,WHITE],
+						title
+					);
+					messages.focus = false;
+				} else {
+					messages.getcmd(cmd);
+				}
+				break;
+			default:
+				break;
+		}
+		return ret;
+	}
+
+	this.close = function () {
+		tree.close();
+		messages.close();
+		frames.top.close();
+	}
+
+	init();
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/messages.js b/xtrn/jeopardized/views/messages.js
new file mode 100644
index 0000000000000000000000000000000000000000..2011259f023ae64cd44af6d79ab71284ff72f0b8
--- /dev/null
+++ b/xtrn/jeopardized/views/messages.js
@@ -0,0 +1,160 @@
+var Messages = function (frame, x, y, w, h, input) {
+
+	var frames = {
+		border : null,
+		messages : null
+	};
+
+	var scrollbar,
+		subscription,
+		scroll = false,
+		focus = false,
+		typeahead,
+		title;
+
+	this.__defineGetter__(
+		'focus',
+		function () {
+			return focus;
+		}
+	);
+
+	this.__defineSetter__(
+		'focus',
+		function (bool) {
+			focus = bool;
+			title.attr = bool ? WHITE : LIGHTGRAY;
+			frames.border.drawBorder(
+				(	bool
+					? [LIGHTBLUE,CYAN,LIGHTCYAN,WHITE]
+					: LIGHTGRAY
+				),
+				title
+			);
+			if (typeof typeahead !== 'undefined' &&
+				typeof typeahead.focus !== 'undefined'
+			) {
+				typeahead.focus = bool;
+			}
+		}
+	);
+
+	function printMessage(msg) {
+		frames.messages.putmsg(
+			(frames.messages.data_height > 0 ? '\r\n' : '') +
+			(	typeof msg.alias === 'undefined'
+				? ''
+				: ('\1h\1c<' + msg.alias + '> \1h\1w')
+			) + msg.message
+		);
+		scroll = true;		
+	}
+
+	function onMessage(update) {
+		log(JSON.stringify(update));
+		printMessage(update.data);
+	}
+
+	function init() {
+		frames.border = new Frame(x, y, w, h, WHITE, frame);
+		title = {
+			x : frames.border.width - 11,
+			y : 1,
+			attr : WHITE,
+			text : 'Messages'
+		};
+		frames.border.drawBorder(LIGHTGRAY, title);
+		frames.messages = new Frame(1, 1, 1, 1, WHITE, frames.border);
+		frames.messages.nest(1, 1);
+		scrollbar = new ScrollBar(frames.messages);
+		frames.border.open();
+		if (typeof input === 'boolean' && input) {
+			frames.messages.height = frames.messages.height - 1;
+			typeahead = new Typeahead(
+				{	x : frames.border.x + 1,
+					y : frames.messages.y + frames.messages.height,
+					frame : frames.border,
+					fg : WHITE,
+					len : frames.messages.width - 1
+				}
+			);
+		}
+		var msgs = database.getMessages();
+		if (typeof msgs !== 'undefined' && Array.isArray(msgs)) {
+			msgs.reverse();
+			msgs.forEach(printMessage);
+		}
+		subscription = database.on('messages.latest', onMessage);
+		var who = [];
+		database.who().forEach(
+			function (u) {
+				if (u.nick === user.alias) return;
+				who.push(u.nick);
+			}
+		);
+		if (who.length > 0) {
+			printMessage(
+				{ message :
+					'\1n\1mOther users online: \1h\1m' +
+						who.join('\1n\1m,\1h\1m ')
+				}
+			);
+		}
+	}
+
+	this.cycle = function () {
+		if (scroll) {
+			scrollbar.cycle();
+			buryCursor = true;
+		}
+		if (typeof typeahead !== 'undefined' && focus) typeahead.cycle();
+		if (system.node_list[bbs.node_num - 1].misc&NODE_MSGW) {
+			var msg = system.get_telegram(user.number).replace(/\r\n$/, '');
+			printMessage({ message : msg });
+			log('telegram');
+		}
+		if (system.node_list[bbs.node_num - 1].misc&NODE_NMSG) {
+			var msg = system.get_node_message(bbs.node_num);
+			msg = msg.replace(/\r\n.n$/, '');
+			printMessage({ message : msg });
+			frames.messages.scroll(0, -1);
+			log('nmsg');
+		}
+	}
+
+	this.getcmd = function (cmd) {
+		switch (cmd) {
+			case KEY_UP:
+				if (frames.messages.data_height > frames.messages.height) {
+					frames.messages.scroll(0, -1);
+				}
+				break;
+			case KEY_DOWN:
+				if (frames.messages.data_height > frames.messages.height &&
+					frames.messages.offset.y + frames.messages.height <
+						frames.messages.data_height
+				) {
+					frames.messages.scroll(0, 1);
+				}
+				break;
+			default:
+				if (typeof typeahead !== 'undefined') {
+					var ret = typeahead.inkey(cmd);
+					if (typeof ret === 'string' && ret.length > 0) {
+						database.postMessage(ret, user);
+					}
+				}
+				break;
+		}
+	}
+
+	this.close = function () {
+		database.off('messages', subscription);
+		frames.border.close();
+	}
+
+	init();
+
+	this.focus = false;
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/news.js b/xtrn/jeopardized/views/news.js
new file mode 100644
index 0000000000000000000000000000000000000000..1c3ab82c479ecfbb504ff540fe0d5dc1a85b4d02
--- /dev/null
+++ b/xtrn/jeopardized/views/news.js
@@ -0,0 +1,71 @@
+var News = function (frame) {
+
+	var frames = {
+		border : null,
+		news : null
+	};
+
+	var scrollbar, news;
+
+	function init() {
+		frames.border = new Frame(1, 1, 1, 1, WHITE, frame);
+		frames.border.nest();
+		frames.border.drawBorder([LIGHTBLUE,CYAN,LIGHTCYAN,WHITE]);
+		frames.news = new Frame(1, 1, 1, 1, WHITE, frames.border);
+		frames.news.nest(1, 1);
+		scrollbar = new ScrollBar(frames.news);
+		frames.news.putmsg(
+			'\1h\1cJeopardized! \1wNews:                             ' +
+			'\1c(\1wUP\1c/\1wDOWN \1nto scroll\1h\1c, ' +
+			'\1wQ\1n to quit\1h\1c)\r\n\r\n'
+		);
+		news = database.getNews();
+		news.reverse();
+		var line = '\1h\1k';
+		for (var n = 0; n < frames.news.width; n++) {
+			line += ascii(196);
+		}
+		news.forEach(
+			function (n) {
+				frames.news.putmsg(n);
+				frames.news.crlf();
+				frames.news.putmsg(line);
+				frames.news.crlf();
+			}
+		);
+		frames.border.open();
+		while (frames.news.up()) { mswait(1); }
+	}
+
+	this.cycle = function () {
+		scrollbar.cycle();
+	}
+
+	this.getcmd = function (cmd) {
+		var ret = true;
+		switch (cmd) {
+			case KEY_UP:
+				frames.news.scroll(0, -1);
+				break;
+			case KEY_DOWN:
+				if (frames.news.data_height > frames.news.height &&
+					frames.news.offset.y + frames.news.height <
+						frames.news.data_height
+				) {
+					frames.news.scroll(0, 1);
+				}
+				break;
+			default:
+				if (cmd !== '') ret = false;
+				break;
+		}
+		return ret;
+	}
+
+	this.close = function () {
+		frames.border.close();
+	}
+
+	init();
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/popup.js b/xtrn/jeopardized/views/popup.js
new file mode 100644
index 0000000000000000000000000000000000000000..53883a92987b199d5cdc42d04738e58cff33da53
--- /dev/null
+++ b/xtrn/jeopardized/views/popup.js
@@ -0,0 +1,71 @@
+var PopUp = function (frame, message, prompt, promptType, state) {
+
+	var frames = {
+		border : null,
+		top : null
+	};
+
+	var typeahead;
+
+	function initDisplay() {
+		frames.border = new Frame(1, 1, 50, 8, WHITE, frame);
+		frames.border.centralize();
+		frames.border.drawBorder([LIGHTBLUE,CYAN,LIGHTCYAN,WHITE]);
+		frames.top = new Frame(1, 1, 1, 1, WHITE, frames.border);
+		frames.top.nest(1, 1);
+		frames.top.centerWrap(message);
+		frames.border.open();
+		if (promptType === PROMPT_ANY || promptType === PROMPT_YN) {
+			frames.top.putmsg('\r\n\r\n');
+			frames.top.center(prompt);
+		} else {
+			typeahead = new Typeahead(
+				{	x : frames.top.x,
+					y : frames.top.y + frames.top.height - 1,
+					frame : frames.top,
+					fg : WHITE,
+					prompt : prompt,
+					len : frames.top.width - prompt.length
+				}
+			);
+		}
+	}
+
+	this.getcmd = function (cmd) {
+		if (typeof typeahead !== 'undefined') cmd = typeahead.inkey(cmd);
+		if (typeof cmd !== 'string') return;
+		switch (promptType) {
+			case PROMPT_YN:
+				cmd = cmd.toUpperCase();
+				cmd = (cmd === 'Y' ? true : (cmd === 'N' ? false : undefined));
+				break;
+			case PROMPT_NUMBER:
+				cmd = (
+					cmd === '' || cmd.search(/[^\d]/) > -1
+					? undefined
+					: parseInt(cmd)
+				);
+				break;
+			case PROMPT_ANY:
+			case PROMPT_TEXT:
+				if (typeof cmd !== 'string' || cmd === '') cmd = undefined;
+				break;
+			default:
+				break;
+		}
+		return cmd;
+	}
+
+	this.cycle = function () {
+		if (typeof typeahead !== 'undefined') typeahead.cycle();
+	}
+
+	this.close = function () {
+		if (typeof typeahead !== 'undefined') typeahead.close();
+		frames.border.close();
+		if (typeof state !== 'undefined') return state;
+	}
+
+	initDisplay();
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/round.js b/xtrn/jeopardized/views/round.js
new file mode 100644
index 0000000000000000000000000000000000000000..0c6eda0ee7b1951f4cb04190c6a6d96e877fb3d5
--- /dev/null
+++ b/xtrn/jeopardized/views/round.js
@@ -0,0 +1,166 @@
+var Round = function(frame, r, game) {
+
+	var board, wager, clue, answer, state;
+	var round = database.getRound(r);
+
+	this.number = r;
+
+	if (r < 3 && countAnswers(game.state.game, r) < 25) {
+		state = STATE_ROUND_BOARD;
+		board = new Board(frame, round, game.state.game);
+	} else if (r === 3 && countAnswers(game.state.game, r) < 1) {
+		state = STATE_ROUND_WAGER;
+		wager = new Wager(frame, game, round, 0, 0);
+		database.markClueAsUsed(user, 3, 0, 0);
+	} else {
+		state = STATE_ROUND_COMPLETE;
+	}
+
+	this.getcmd = function (cmd) {
+
+		var progress = RET_ROUND_CONTINUE;
+
+		switch (state) {
+
+			case STATE_ROUND_BOARD:
+				if (cmd.toUpperCase() === 'Q') {
+					progress = RET_ROUND_QUIT;
+				} else {
+					var ret = board.getcmd(cmd);
+					if (typeof ret === 'object' &&
+						typeof ret.column === 'number' &&
+						typeof ret.row === 'number'
+					) {
+						database.markClueAsUsed(user, r, ret.column, ret.row);
+						if (round[ret.column].clues[ret.row].dd) {
+							state = STATE_ROUND_WAGER;
+							wager = new Wager(
+								frame, game, round, ret.column, ret.row
+							);
+						} else {
+							state = STATE_ROUND_CLUE;
+							clue = new Clue(frame, round, ret.column, ret.row);
+						}
+					}
+				}
+				break;
+
+			case STATE_ROUND_WAGER:
+				var ret = wager.getcmd(cmd);
+				if (typeof ret === 'number') {
+					game.state.wager = ret;
+					wager.close();
+					state = STATE_ROUND_CLUE;
+					clue = new Clue(frame, round, wager.category, wager.clue);
+				}
+				break;
+
+			case STATE_ROUND_CLUE:
+				var ret = clue.getcmd(cmd);
+				if (typeof ret === 'string') {
+					state = STATE_ROUND_ANSWER;
+					clue.close();
+					database.submitAnswer(
+						user,
+						r,
+						clue.category,
+						clue.clue,
+						ret,
+						game.state.wager
+					);
+					answer = new Answer(
+						frame,
+						round,
+						clue.category,
+						clue.clue,
+						ret,
+						game
+					);
+				}
+				break;
+
+			case STATE_ROUND_ANSWER:
+				var ret = answer.getcmd(cmd);
+				if (ret) {
+					answer.close();
+					game.state.wager = null;
+					var ac = countAnswers(game.state.game, r);
+					if (r < 3) {
+						state = STATE_ROUND_BOARD;
+						if (ac >= 10 && game.state.game.winnings >= 0) {
+							progress = RET_ROUND_NEXT;
+						} else if (ac >= 25) {
+							progress = RET_ROUND_STOP;
+						}
+					} else if (r >= 3) {
+						progress = RET_ROUND_LAST;
+					}
+				}
+				break;
+
+			case STATE_ROUND_COMPLETE:
+				progress = RET_ROUND_STOP;
+				break;
+
+			default:
+				break;
+
+		}
+
+		return progress;
+
+	}
+
+	this.cycle = function () {
+
+		switch (state) {
+
+			case STATE_ROUND_BOARD:
+				board.cycle();
+				break;
+
+			case STATE_ROUND_WAGER:
+				wager.cycle();
+				break;
+			
+			case STATE_ROUND_CLUE:
+				clue.cycle();
+				if (clue.expired) {
+					clue.close();
+					database.submitAnswer(
+						user,
+						r,
+						clue.category,
+						clue.clue,
+						'!wrong_answer!',
+						game.state.wager
+					);
+					answer = new Answer(
+						frame,
+						round,
+						clue.category,
+						clue.clue,
+						'!wrong_answer!',
+						game
+					);
+					state = STATE_ROUND_ANSWER;
+				}
+				break;
+			
+			case STATE_ROUND_ANSWER:
+				answer.cycle();
+				break;
+			
+			default:
+				break;
+	
+		}
+
+	}
+
+	this.close = function () {
+		if (typeof clue !== 'undefined') clue.close();
+		if (typeof board !== 'undefined') board.close();
+	}
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/scoreboard.js b/xtrn/jeopardized/views/scoreboard.js
new file mode 100644
index 0000000000000000000000000000000000000000..497acffc55ec81490f1d7f98754633ed63095d70
--- /dev/null
+++ b/xtrn/jeopardized/views/scoreboard.js
@@ -0,0 +1,129 @@
+var Scoreboard = function (frame) {
+
+	var lFrame,
+		layout,
+		tabTodayMoney,
+		tabTotalMoney;
+
+	var header = format(
+		"%-20s%-25s%19s   Answers\r\n",
+		"Alias", "System", "Winnings"
+	);
+
+	function formatLine(record) {
+		var us = base64_decode(record.id).split('@');
+		return format(
+			"\1h\1c%-20s\1n\1c%-25s\1h%s%19s\1g%6s\1w:\1r%-6s\r\n",
+			(	us[0].length > 19
+				? (us[0].substr(0, 16) + '\1k...')
+				: us[0].substr(0, 19)
+			),
+			(	us[1].length > 24
+				? (us[1].substr(0, 21) + '\1k...')
+				: us[1].substr(0, 24)
+			),
+			(record.winnings >= 0 ? '\1g' : '\1r'),
+			'$' + record.winnings,
+			record.answers.correct,
+			record.answers.incorrect
+		);
+	}
+
+	function initDisplay() {
+		lFrame = new Frame(1, 1, 1, 1, WHITE, frame);
+		lFrame.nest();
+		lFrame.height = lFrame.height - 1;
+
+		var hFrame = new Frame(
+			lFrame.x,
+			lFrame.y + lFrame.height,
+			lFrame.width,
+			1,
+			BG_BLUE|WHITE,
+			lFrame
+		);
+		hFrame.center(
+			'\1cLEFT\1w/\1cRIGHT \1wto switch tabs : ' +
+			'\1cUP\1w/\1cDOWN \1wto scroll : ' +
+			'\1cQ \1wto quit'
+		);
+
+		layout = new Layout(lFrame);
+		layout.colors.view_fg = WHITE;
+		layout.colors.border_fg = LIGHTCYAN;
+		layout.colors.title_fg = WHITE;
+		layout.colors.tab_fg = WHITE;
+		layout.colors.tab_bg = BG_BLUE;
+		layout.colors.inactive_tab_bg = BG_BLUE;
+		layout.colors.inactive_tab_fg = LIGHTGRAY;
+
+		var view = layout.addView(
+			"Scoreboard",
+			lFrame.x,
+			lFrame.y,
+			lFrame.width,
+			lFrame.height
+		);
+
+		tabTodayMoney = view.addTab('Today\'s Game', 'FRAME');
+		tabTotalMoney = view.addTab('Total', 'FRAME');
+		tabTodayMoney._scrollbar = new ScrollBar(tabTodayMoney.frame);
+		tabTotalMoney._scrollbar = new ScrollBar(tabTotalMoney.frame);
+		tabTodayMoney.frame.putmsg(header);
+		tabTotalMoney.frame.putmsg(header);
+
+		lFrame.open();
+		layout.open();
+
+	}
+
+	function initData() {
+		var data = database.getRankings();
+		data.today.money.forEach(
+			function (d) {
+				tabTodayMoney.frame.putmsg(formatLine(data.today.data[d]));
+			}
+		);
+		data.total.money.forEach(
+			function (d) {
+				tabTotalMoney.frame.putmsg(formatLine(data.total.data[d]));
+			}
+		);
+	}
+
+	function init() {
+		initDisplay();
+		initData();
+	}
+
+	this.getcmd = function (cmd) {
+		var ret = true;
+		switch (cmd.toUpperCase()) {
+			case '\x09':
+			case KEY_LEFT:
+			case KEY_RIGHT:
+			case KEY_UP:
+			case KEY_DOWN:
+				layout.getcmd(cmd);
+				break;
+			case 'Q':
+				ret = false;
+				break;
+			default:
+				break;
+		}
+		return ret;
+	}
+
+	this.cycle = function () {
+		layout.current.current._scrollbar.cycle();
+		layout.cycle();
+	}
+
+	this.close = function () {
+		layout.close();
+	}
+
+	init();
+
+}
\ No newline at end of file
diff --git a/xtrn/jeopardized/views/wager.js b/xtrn/jeopardized/views/wager.js
new file mode 100644
index 0000000000000000000000000000000000000000..6fc10e6e3f7f6e6ecae67d77265e29083c8c73b0
--- /dev/null
+++ b/xtrn/jeopardized/views/wager.js
@@ -0,0 +1,38 @@
+var Wager = function (frame, game, round, category, clue) {
+
+	this.category = category;
+	this.clue = clue;
+
+	if (round.number < 3) {
+		var msg = "You've been DOUBLY JEOPARDIZED!";
+		var max = Math.max(
+			round[category].clues[round[category].clues.length - 1].value,
+			game.winnings
+		);
+	} else {
+		var msg = "FINAL ROUND";
+		var max = Math.max(1000, game.winnings);
+	}
+
+	msg += '\r\n' + 
+		'Category: ' + round[category].name + '\r\n' +
+		'Please enter your bet: $5 - $' + max;
+
+	var popUp = new PopUp(frame, msg, '$', PROMPT_NUMBER, STATE_ROUND_WAGER);
+
+	this.cycle = function () {
+		popUp.cycle();
+	}
+
+	this.getcmd = function (cmd) {
+		var ret = popUp.getcmd(cmd);
+		if (typeof ret !== 'number') return;
+		if (ret < 5 || ret > max) return;
+		return ret;
+	}
+
+	this.close = function () {
+		popUp.close();
+	}
+
+}
\ No newline at end of file