A Toy Bug Tracker in JavaScript

The JavaScript code below implements a rudimentary bug tracker, or, perhaps more accurately, a task tracker. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

It’s not actually useful in its current form, of course, as all the users are hardcoded and any tasks added during the session aren’t persisted anywhere. I’d also like to implement parent-child relationships among tasks and add support for generalized attributes. This program is intended mostly as a basis for further work.


<html>
<body>

<!-- ui -->

<div id="divMain"></div>


<!-- views -->

<div id="divViews" style="display:none">

	<div id="divPageTaskSearchView">

		<label><b>Task Search</b></label>
		<br />

		<label>Text to Search for:</label>
		<input id="inputTextToSearchFor"></input>
		<button id="buttonSearch">Search</button>
		<br />

		<label>Tasks Found:</label>
		<label id="labelTasksFoundCount">0</label>
		<br />

		<div>
			<table width="100%">
				<thead style="text-align:left">	
					<th>ID</th>
					<th>Title</th>
					<th>Time Created</th>
					<th>Time Updated</th>
					<th>Status</th>
					<th>User Assigned</th>
					<th>Actions</th>
				</thead>
				<tbody id="tbodyTasksFound">
				</tbody>
			</table>
		</div>
	</div>

	<div id="divPageTaskDetailView">

		<label><b>Task Detail</b></label>
		<br />

		<label>ID:</label><input id="inputTaskID" readonly="readonly"></input><br />
		<label>Title:</label><input id="inputTaskTitle"></input><br />
		<label>Time Created:</label><input id="inputTaskTimeCreated" readonly="readonly"></input><br />
		<label>Time Updated:</label><input id="inputTaskTimeUpdated" readonly="readonly"></input><br />
		<label>Status:</label><select id="selectTaskStatus"></select><br />
		<label>User Assigned:</label><select id="selectTaskUserAssigned"></select><br />

		<label>Comments:</label><br />
		<div id="divTaskComments"></div>
		
		<label>Comment to Add:</label>
		<input id="inputTaskCommentToAddText"></input>
		<button id="buttonTaskCommentAdd">Add Comment</button>
		<br />

		<button id="buttonTaskSave">Save</button>
		<button id="buttonGoToTaskSearch">Back to Task Search</button>
	</div>

	<div id="divPageUserLoginView">

		<label><b>User Login</b></label>
		<br />

		<label>Username:</label><input id="inputUserName"></input><br />
		<label>Password:</label><input id="inputUserPassword" type="password"></input><br />

		<button id="buttonUserLogIn">Log In</button>
	</div>

	<div id="divPageUserDetailView">

		<label><b>User Detail</b></label>
		<br />

		<label>User:</label><input id="inputUserName" readonly="readonly"></input><br />

		<label>Tasks Assigned:</label><br />
		<div>
			<table width="100%">
				<thead style="text-align:left">	
					<th>ID</th>
					<th>Title</th>
					<th>Time Created</th>
					<th>Time Updated</th>
					<th>Status</th>
					<th>Actions</th>
				</thead>
				<tbody id="tbodyTasksAssigned">
				</tbody>
			</table>
		</div>

		<button id="buttonGoToTaskSearch">Search For Tasks</button>
	</div>

</div>

<!-- code -->

<script type="text/javascript">

// main

function main()
{
	var users = 
	[
		new User(1, "one", "one"),
		new User(2, "two", "two"),
		new User(3, "three", "three"),
	];

	var now = new Date();

	var statusCreatedID = TaskStatus.Instances.Created.id;

	var tasks =
	[
		new Task
		(
			1, // id
			"first", // title
			now, // timeCreated
			now, // timeUpdated
			TaskStatus.Instances.Defined.id,
			1, // userAssignedID
			// comments
			[
				new TaskComment
				(
					now,
					2, // userID
					"This is the first bug."
				),
				new TaskComment
				(
					now,
					3, // userID
					"This is some expert analysis."
				)
			]
		),

		new Task
		(
			2, // id
			"second", // title
			now, // timeCreated
			now, // timeUpdated
			statusCreatedID,
			null, // userAssignedID
			// comments
			[
				new TaskComment
				(
					now,
					2, // userID
					"This is the second bug."
				)
			]
		),


		new Task
		(
			3, // id
			"third", // title
			now, // timeCreated
			now, // timeUpdated
			statusCreatedID,
			null, // userAssignedID
			// comments
			[
				new TaskComment
				(
					now,
					3, // userID
					"This is the third bug."
				)
			]
		),
	];

	var database = new Database(users, tasks);
	var userSession = new UserSession();

	Globals.Instance.initialize
	(
		database, userSession
	);	
}

// extensions

function ArrayExtensions()
{
	// Extension class.
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			if (isNaN(key) == false)
			{
				key = "_" + key;
			}
			this[key] = element;
		}
		return this;
	}

	// cloneable

	Array.prototype.clone = function()
	{
		var returnValues = [];

		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var elementCloned = element.clone();
			returnValues.push(elementCloned);
		}

		return returnValues;
	}

	Array.prototype.overwriteWith = function(other)
	{
		if (this.length > other.length)
		{
			this.length = other.length;
		}

		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var elementToOverwriteWith = other[i];
			element.overwriteWith(elementToOverwriteWith);
		}

		var numberOfElementsToClone = other.length - this.length;
		for (var i = 0; i < numberOfElementsToClone; i++)
		{
			var elementToClone = other[this.length + i];
			var elementCloned = elementToClone.clone();
			this.push(elementCloned);
		}

		return this;
	}
}

function StringExtensions()
{
	// Extension class.
}
{
	String.prototype.contains = function(substring)
	{
		return this.indexOf(substring) >= 0;
	}
}

// classes

function Database(users, tasks)
{
	this.users = users.addLookups("id").addLookups("name");
	this.tasks = tasks.addLookups("id");
}
{
	Database.prototype.taskGetByID = function(id)
	{
		var returnValue = this.tasks["_" + id];
		if (returnValue != null)
		{
			returnValue = returnValue.clone();
		}
		return returnValue;
	}

	Database.prototype.taskSave = function(task)
	{
		if (task.id == null)
		{
			task.id = this.tasks.length;
			this.tasks.push(task);
		}
		else
		{
			var taskExisting = this.tasks["_" + task.id];
			taskExisting.overwriteWith(task);
		}

		this.tasks.addLookups("id");
	}

	Database.prototype.tasksGetAll = function()
	{
		return this.tasks.clone();
	}

	Database.prototype.tasksGetByUserAssignedID = function(userAssignedID)
	{
		var returnValues = [];

		for (var i = 0; i < this.tasks.length; i++)
		{
			var task = this.tasks[i];
			if (task.userAssignedID == userAssignedID)
			{
				returnValues.push(task);
			}
		}

		return returnValues.clone();
	}

	Database.prototype.userGetByID = function(id)
	{
		var returnValue = this.users["_" + id];
		if (returnValue != null)
		{
			returnValue = returnValue.clone();
		}
		return returnValue;

	}

	Database.prototype.userGetByName = function(name)
	{
		var returnValue = this.users[name];
		if (returnValue != null)
		{
			returnValue = returnValue.clone();
		}
		return returnValue;
	}

	Database.prototype.usersGetAll = function()
	{
		return this.users.clone();
	}

	Database.prototype.userSave = function(user)
	{
		if (user.id == null)
		{
			user.id = this.users.length;
			this.users.push(user);
		}
		else
		{
			var userExisting = this.users["_" + user.id];
			userExisting.overwriteWith(user);
		}

		this.users.addLookups("id").addLookups("name");
	}
}

function Globals()
{
	// Do nothing.	
}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(database, userSession)
	{
		this.database = database;
		this.userSession = userSession;

		this.userSession.initialize();
	}
}

function PageTaskDetail(task)
{
	this.task = task;

	this.taskCommentToAddText = "";
}
{
	PageTaskDetail.prototype.taskCommentAdd = function()
	{
		if (this.taskCommentToAddText != "")
		{
			var commentToAdd = new TaskComment
			(
				new Date(),
				Globals.Instance.userSession.user.id,
				this.taskCommentToAddText
			);
			this.task.comments.push(commentToAdd);

			this.taskCommentToAddText = "";

			this.domElementUpdate();
		}
	}

	PageTaskDetail.prototype.taskSave = function()
	{
		this.task.timeUpdated = new Date();
		Globals.Instance.database.taskSave(this.task);
	}

	// dom

	PageTaskDetail.prototype.domElementUpdate = function()
	{
		var d = document;

		var domElement = this._domElement;

		var task = this.task;
		var database = Globals.Instance.database;

		if (domElement == null)
		{
			domElement = d.createElement("div");

			domElement.innerHTML =
				d.getElementById("divPageTaskDetailView").innerHTML;

			var divMain = d.getElementById("divMain");
			divMain.appendChild(domElement);

			var selectTaskStatus = d.getElementById("selectTaskStatus");
			var statusesAll = TaskStatus.Instances._All;
			for (var i = 0; i < statusesAll.length; i++)
			{
				var status = statusesAll[i];
				var statusAsDomElement = status.domElementUpdate();
				selectTaskStatus.appendChild(statusAsDomElement);
			}
			selectTaskStatus.onchange = function(event)
			{
				this.task.statusID = event.target.value;
			}.bind(this);

			var selectTaskUserAssigned = d.getElementById("selectTaskUserAssigned");
			var usersAll = database.usersGetAll();
			for (var i = 0; i < usersAll.length; i++)
			{
				var user = usersAll[i];
				var userAsDomElement = user.domElementUpdate();
				selectTaskUserAssigned.appendChild(userAsDomElement);
			}
			selectTaskUserAssigned.onchange = function(event)
			{
				this.task.userAssignedID = event.target.value;
			}.bind(this);

			var page = this;
			d.getElementById("inputTaskCommentToAddText").onchange = function(event)
			{
				page.taskCommentToAddText = event.target.value;
			}
			d.getElementById("buttonTaskCommentAdd").onclick = this.taskCommentAdd.bind(this);
			d.getElementById("buttonTaskSave").onclick = this.taskSave.bind(this);
			d.getElementById("buttonGoToTaskSearch").onclick = function()
			{
				Globals.Instance.userSession.pageCurrentSet
				(
					new PageTaskSearch()
				);
			}

			this._domElement = domElement;
		}

		d.getElementById("inputTaskID").value = task.id;
		d.getElementById("inputTaskTitle").value = task.title;
		d.getElementById("inputTaskTimeCreated").value = task.timeCreated.toISOString();
		d.getElementById("inputTaskTimeUpdated").value = task.timeUpdated.toISOString();
		d.getElementById("selectTaskStatus").value = task.statusID;
		d.getElementById("selectTaskUserAssigned").value = task.userAssignedID;

		var divTaskComments = d.getElementById("divTaskComments");
		divTaskComments.innerHTML = "";
		for (var i = 0; i < task.comments.length; i++)
		{
			var comment = task.comments[i];
			var commentAsDomElement = comment.domElementUpdate();
			divTaskComments.appendChild(commentAsDomElement);
		}

		d.getElementById("inputTaskCommentToAddText").value = "";

		return domElement;
	}
}

function PageTaskSearch()
{
	this.textToSearchFor = "";
	this.tasksFound = [];	
}
{
	PageTaskSearch.prototype.search = function()
	{
		this.tasksFound.length = 0;

		var tasksAll = Globals.Instance.database.tasksGetAll();

		if (this.textToSearchFor == "")
		{
			this.tasksFound = tasksAll.slice(); // copy
		}
		else
		{
			for (var i = 0; i < tasksAll.length; i++)
			{
				var task = tasksAll[i];
				var doesTaskContainText
					= task.doesContainText(this.textToSearchFor);
				if (doesTaskContainText)
				{
					this.tasksFound.push(task);
				}
			}
		}

		this.domElementUpdate();
	}

	// dom

	PageTaskSearch.prototype.domElementUpdate = function()
	{
		var d = document;

		var domElement = this._domElement;

		if (domElement == null)
		{
			domElement = d.createElement("div");

			domElement.innerHTML =
				d.getElementById("divPageTaskSearchView").innerHTML;

			var divMain = d.getElementById("divMain");
			divMain.appendChild(domElement);

			var inputTextToSearchFor = d.getElementById("inputTextToSearchFor");
			inputTextToSearchFor.onchange = function(event)
			{
				this.textToSearchFor = event.target.value;
			}.bind(this);

			var buttonSearch = d.getElementById("buttonSearch");
			buttonSearch.onclick = this.search.bind(this);

			this._domElement = domElement;
		}

		var labelTasksFoundCount = d.getElementById("labelTasksFoundCount");
		labelTasksFoundCount.innerHTML = this.tasksFound.length;

		var tbodyTasksFound = d.getElementById("tbodyTasksFound");
		tbodyTasksFound.innerHTML = "";
		for (var i = 0; i < this.tasksFound.length; i++)
		{
			var taskFound = this.tasksFound[i];
			var taskFoundAsDomElement = taskFound.domElementUpdate(true);
			tbodyTasksFound.appendChild(taskFoundAsDomElement);
		}

		return domElement;
	}
}

function PageUserDetail(user)
{
	this.user = user;
}
{
	// dom 

	PageUserDetail.prototype.domElementUpdate = function()
	{
		var d = document;

		var domElement = this._domElement;

		if (domElement == null)
		{
			domElement = d.createElement("div");

			domElement.innerHTML =
				d.getElementById("divPageUserDetailView").innerHTML;

			d.getElementById("divMain").appendChild(domElement);

			d.getElementById("inputUserName").value = this.user.name;

			d.getElementById("buttonGoToTaskSearch").onclick = function()
			{
				Globals.Instance.userSession.pageCurrentSet
				(
					new PageTaskSearch()
				);
			}

			this._domElement = domElement;
		}

		var tasksAssigned = this.user.tasksAssigned();

		var tbodyTasksAssigned = d.getElementById("tbodyTasksAssigned");
		tbodyTasksAssigned.innerHTML = "";
		for (var i = 0; i < tasksAssigned.length; i++)
		{
			var task = tasksAssigned[i];
			var taskAsDomElement = task.domElementUpdate(false);
			tbodyTasksAssigned.appendChild(taskAsDomElement);
		}

		return domElement;
	}
}

function PageUserLogin()
{
	this.userName = "";
	this.password = "";
}
{
	// dom

	PageUserLogin.prototype.domElementUpdate = function()
	{
		var d = document;

		var domElement = this._domElement;

		if (domElement == null)
		{
			domElement = d.createElement("div");

			domElement.innerHTML =
				d.getElementById("divPageUserLoginView").innerHTML;

			var divMain = d.getElementById("divMain");
			divMain.appendChild(domElement);

			d.getElementById("inputUserName").onchange = function(event)
			{
				this.userName = event.target.value;
			}.bind(this);

			d.getElementById("inputUserPassword").onchange = function(event)
			{
				this.password = event.target.value;
			}.bind(this);

			d.getElementById("buttonUserLogIn").onclick = function(event)
			{
				var userFound = Globals.Instance.database.userGetByName(this.userName);
				if (userFound == null || this.password != userFound.password)
				{
					alert("Username or password not valid.");
				}
				else
				{
					var session = Globals.Instance.userSession;
					session.user = userFound;
					session.pageCurrentSet(new PageUserDetail(userFound));
				}
			}.bind(this);

			this._domElement = domElement;
		}

		return domElement;
	}
}

function Task
(
	id,
	title,
	timeCreated,
	timeUpdated,
	statusID,
	userAssignedID,
	comments,
	parentID
)
{
	this.id = id;
	this.title = title;
	this.timeCreated = timeCreated;
	this.timeUpdated = timeUpdated;
	this.statusID = statusID;
	this.userAssignedID = userAssignedID;
	this.comments = comments;
	this.parentID = parentID;
}
{
	Task.prototype.doesContainText = function(textToFind)
	{
		var returnValue = this.title.contains(textToFind);

		if (returnValue == false)
		{
			for (var i = 0; i < this.comments.length; i++)
			{
				var comment = this.comments[i];
				returnValue = comment.doesContainText(textToFind);
				if (returnValue == true)
				{
					break;
				}
			}
		}

		return returnValue;
	}

	Task.prototype.status = function()
	{
		return TaskStatus.Instances._All["_" + this.statusID];
	}

	Task.prototype.userAssigned = function()
	{
		var database = Globals.Instance.database;
		var userAssigned = database.userGetByID(this.userAssignedID);
		return userAssigned;
	}

	// cloneable

	Task.prototype.clone = function()
	{
		return new Task
		(
			this.id,
			this.title,
			this.timeCreated,
			this.timeUpdated,
			this.statusID,
			this.userAssignedID,
			this.comments.clone(),
			this.parentID
		);
	}

	Task.prototype.overwriteWith = function(other)
	{
		this.id = other.id;
		this.title = other.title;
		this.timeCreated = other.timeCreated;
		this.timeUpdated = other.timeUpdated;
		this.statusID = other.statusID;
		this.userAssignedID = other.userAssignedID;
		this.comments.overwriteWith(other.comments);
		this.parentID = other.parentID;

		return this;
	}

	// dom

	Task.prototype.domElementUpdate = function(includeUserAssigned)
	{
		var d = document;

		var domElement = d.createElement("tr");

		var userAssigned = this.userAssigned();

		var fieldsToInclude = 
		[ 
			this.id,
			this.title,
			this.timeCreated.toISOString(),
			this.timeUpdated.toISOString(),
			this.status().name
		];

		if (includeUserAssigned)
		{
			fieldsToInclude.push
			(
				userAssigned == null ? "[none]" : userAssigned.name
			);
		}

		for (var i = 0; i < fieldsToInclude.length; i++)
		{
			var field = fieldsToInclude[i];
			var fieldAsTd = d.createElement("td");
			fieldAsTd.innerHTML = field;
			domElement.appendChild(fieldAsTd);
		}

		var linkTaskView = d.createElement("a");
		linkTaskView.innerHTML = "View";
		linkTaskView.href = "#";
		var taskID = this.id;
		linkTaskView.onclick = function()
		{
			var session = Globals.Instance.userSession;
			var task = Globals.Instance.database.taskGetByID(taskID);
			session.pageCurrentSet(new PageTaskDetail(task));
		}
		var tdTaskView = d.createElement("td");
		tdTaskView.appendChild(linkTaskView);
		domElement.appendChild(tdTaskView);

		return domElement;
	}
}

function TaskComment(timeCreated, userID, text)
{
	this.timeCreated = timeCreated;
	this.userID = userID;
	this.text = text;
}
{
	TaskComment.prototype.doesContainText = function(textToFind)
	{
		return this.text.contains(textToFind);
	}

	TaskComment.prototype.user = function()
	{
		return Globals.Instance.database.userGetByID(this.userID);
	}

	// cloneable

	TaskComment.prototype.clone = function()
	{
		return new TaskComment(this.timeCreated, this.userID, this.text);
	}

	TaskComment.prototype.overwriteWith = function(other)
	{
		this.timeCreated = other.timeCreated;
		this.userID = other.userID;
		this.text = other.text;

		return this;
	}

	// dom

	TaskComment.prototype.domElementUpdate = function()
	{
		var returnValue = document.createElement("div");
		returnValue.innerHTML =
			this.timeCreated.toISOString()
			+ " - " + this.user().name
			+ "<br />"
			+ this.text
			+ "<br />";
		return returnValue;
	}
}

function TaskStatus(id, name)
{
	this.id = id;
	this.name = name;
}
{
	TaskStatus.Instances = new TaskStatus_Instances();

	function TaskStatus_Instances()
	{
		this.Created = new TaskStatus(1, "Created");
		this.Defined = new TaskStatus(2, "Defined");
		this.Implemented = new TaskStatus(3, "Implemented");
		this.Verified = new TaskStatus(4, "Verified");
		this.Delivered = new TaskStatus(5, "Delivered");

		this.Blocked = new TaskStatus(6, "Blocked");
		this.Cancelled = new TaskStatus(7, "Cancelled");

		this._All = 
		[
			this.Created,
			this.Defined,
			this.Implemented,
			this.Verified,
			this.Delivered,
			this.Blocked,
			this.Cancelled,
		].addLookups("id");
	}

	// dom

	TaskStatus.prototype.domElementUpdate = function()
	{
		var returnValue = document.createElement("option");
		returnValue.innerHTML = this.name;
		returnValue.value = this.id;
		return returnValue;
	}
}

function User(id, name, password)
{
	this.id = id;
	this.name = name;
	this.password = password;
}
{
	User.prototype.tasksAssigned = function()
	{
		return Globals.Instance.database.tasksGetByUserAssignedID(this.id);
	}

	// cloneable

	User.prototype.clone = function()
	{
		return new User(this.id, this.name, this.password);
	}

	User.prototype.overwriteWith = function(other)
	{
		this.id = other.id;
		this.name = other.name;
		this.password = other.password;

		return this;
	}

	// dom

	User.prototype.domElementUpdate = function()
	{
		var returnValue = document.createElement("option");
		returnValue.innerHTML = this.name;
		returnValue.value = this.id;
		return returnValue;
	}
}

function UserSession(user)
{
	this.user = user;
}
{
	UserSession.prototype.initialize = function()
	{
		this.pageCurrentSet(new PageUserLogin());
	}

	UserSession.prototype.pageCurrentSet = function(value)
	{	
		this.pageCurrent = value;

		var divMain = document.getElementById("divMain");
		divMain.innerHTML = "";
		var pageCurrentAsDomElement = this.pageCurrent.domElementUpdate();
		divMain.appendChild(pageCurrentAsDomElement);
	}
}

// run

main();

</script>
</body>
</html>


Advertisements
Posted in Uncategorized | Tagged , , | 1 Comment

Exploring the ZIP File Format in JavaScript

The JavaScript program below, when run, prompts the user to upload a file in .ZIP format. Then the file is parsed and its internal structure is displayed as text in the JSON format.

Note that this code does not actually decompress the data, though it does display the compressed data for each internal file in hexadecimal format.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.


<html>
<body>

<div id="divUI">
	<label>ZIP File to Load:</label><br />
	<input type="file" onchange="inputFile_Changed(this);"></input><br />
	<label>File Contents as JSON:</label><br />
	<textarea id="textareaFileAsJSON" cols="80" rows="20"></textarea>
</div>

<script type="text/javascript">

// ui event handlers

function inputFile_Changed(inputFile)
{
	var file = inputFile.files[0];
	if (file != null)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(event)
		{
			var fileAsBinaryString = event.target.result;
			var fileAsBytes = ByteHelper.binaryStringToBytes(fileAsBinaryString);
			var fileAsZipFile = ZipFile.fromBytes(fileAsBytes);
			var fileAsJSON = fileAsZipFile.toStringJSON();
			var textareaFileAsJSON = document.getElementById("textareaFileAsJSON");
			textareaFileAsJSON.value = fileAsJSON;
		}
		fileReader.readAsBinaryString(file);
	}
}

// classes

function ByteHelper()
{
	// static class
}
{
	ByteHelper.binaryStringToBytes = function(binaryString)
	{
		var bytesSoFar = [];
		for (var i = 0; i < binaryString.length; i++)
		{
			var byte = binaryString.charCodeAt(i);
			bytesSoFar.push(byte);
		}
		return bytesSoFar;
	}

	// "LE" = "Little Endian"

	ByteHelper.bytesToInteger16LE = function(bytes) 
	{
		return (bytes[1] << 8) | bytes[0];
	}

	ByteHelper.bytesToInteger32LE = function(bytes)
	{
		return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0];
	}

	ByteHelper.bytesToStringHexadecimal = function(bytes)
	{
		var returnValue = "";
		for (var i = 0; i < bytes.length; i++)
		{
			var byte = bytes[i];
			var byteAsString = byte.toString(16);
			returnValue += byteAsString;
		}
		return returnValue;
	}

	ByteHelper.bytesToStringUTF8 = function(bytes)
	{
		var returnValue = "";
		for (var i = 0; i < bytes.length; i++)
		{
			var byte = bytes[i];
			var byteAsString = String.fromCharCode(byte);
			returnValue += byteAsString;
		}
		return returnValue;
	}
}

function ByteStream(bytes)
{
	this.bytes = bytes;
	this.byteOffset = 0;

	this.byteBuffer = [];
}
{
	ByteStream.prototype.hasMore = function()
	{
		return this.byteOffset < this.bytes.length;
	}

	ByteStream.prototype.readByte = function()
	{
		var byteRead = this.bytes[this.byteOffset];
		this.byteOffset++;

		return byteRead;
	}

	ByteStream.prototype.readBytes = function(byteCount)
	{
		return this.readBytesIntoBuffer(byteCount).slice();
	}

	ByteStream.prototype.readBytesIntoBuffer = function(byteCount)
	{
		this.byteBuffer.length = 0;
		for (var i = 0; i < byteCount; i++)
		{
			var byteRead = this.readByte();
			this.byteBuffer.push(byteRead);
		}
		return this.byteBuffer;
	}

	// "LE" = "Little Endian"

	ByteStream.prototype.readInteger16LE = function()
	{
		return ByteHelper.bytesToInteger16LE(this.readBytesIntoBuffer(2));
	}

	ByteStream.prototype.readInteger32LE = function()
	{
		return ByteHelper.bytesToInteger32LE(this.readBytesIntoBuffer(4));
	}

	ByteStream.prototype.readStringHexadecimal = function(length)
	{
		return ByteHelper.bytesToStringHexadecimal(this.readBytesIntoBuffer(length));
	}

	ByteStream.prototype.readStringUTF8 = function(length)
	{
		return ByteHelper.bytesToStringUTF8(this.readBytesIntoBuffer(length));
	}
}

function ZipFile(entriesLocal, entriesCentralDirectory, centralDirectoryEndRecord)
{
	this.entriesLocal = entriesLocal;
	this.entriesCentralDirectory = entriesCentralDirectory;
	this.centralDirectoryEndRecord = centralDirectoryEndRecord;
}
{
	ZipFile.fromBytes = function(bytes)
	{
		var entriesLocal = [];
		var entriesCentralDirectory = [];
		var centralDirectoryEndRecord = null;
	
		var reader = new ByteStream(bytes);

		while (reader.hasMore() == true)
		{
			var signature = reader.readInteger32LE();
			if (signature == ZipFileLocalFileHeader.Signature)
			{
				// It's a local file header.
				var versionNeededToExtract = reader.readInteger16LE();
				var flags = reader.readInteger16LE();
				var compressionMethod = reader.readStringUTF8(2);

				var timeLastModified = ZipFile.fromBytes_ReadTimeFromByteStream(reader);

				var crc32 = reader.readInteger32LE();
				var sizeCompressedInBytes = reader.readInteger32LE();
				var sizeUncompressedInBytes = reader.readInteger32LE();
				var filenameLength = reader.readInteger16LE();
				var extraFieldLength = reader.readInteger16LE();
				var filename = reader.readStringUTF8(filenameLength);
				var extraFieldAsHexadecimal = reader.readStringHexadecimal(extraFieldLength);

				var entryHeader = new ZipFileLocalFileHeader
				(
					signature,
					versionNeededToExtract,
					flags,
					compressionMethod,
					timeLastModified,
					crc32,
					sizeCompressedInBytes,
					sizeUncompressedInBytes,
					filename,
					extraFieldAsHexadecimal
				);

				var entryDataCompressedAsStringHexadecimal = 
					reader.readStringHexadecimal(sizeCompressedInBytes);

				var entry = new ZipFileLocalFileEntry(entryHeader, entryDataCompressedAsStringHexadecimal);

				entriesLocal.push(entry);
			}
			else if (signature == ZipFileCentralDirectoryEntry.Signature)
			{
				// It's a central directory file header.
				var versionMadeBy = reader.readInteger16LE();				
				var versionNeededToExtract = reader.readInteger16LE();
				var flags = reader.readInteger16LE();
				var compressionMethod = reader.readStringUTF8(2);
				var timeLastModified = ZipFile.fromBytes_ReadTimeFromByteStream(reader);
				var crc32 = reader.readInteger32LE();
				var sizeCompressedInBytes = reader.readInteger32LE();
				var sizeUncompressedInBytes = reader.readInteger32LE();
				var filenameLength = reader.readInteger16LE();
				var extraFieldLength = reader.readInteger16LE();
				var fileCommentLength = reader.readInteger16LE();
				var diskNumber = reader.readInteger16LE();
				var fileAttributesInternal = reader.readInteger16LE();
				var fileAttributesExternal = reader.readInteger32LE();
				var offsetOfLocalHeader = reader.readInteger32LE();
				var filename = reader.readStringUTF8(filenameLength);
				var extraFieldAsHexadecimal = reader.readStringHexadecimal(extraFieldLength);
				var fileComment = reader.readStringUTF8(fileCommentLength);

				var entry = new ZipFileCentralDirectoryEntry
				(
					signature,
					versionMadeBy,
					versionNeededToExtract,
					flags,
					compressionMethod,
					timeLastModified,
					crc32,
					sizeCompressedInBytes,
					sizeUncompressedInBytes,
					filename,
					extraFieldAsHexadecimal,
					fileComment,
					diskNumber,
					fileAttributesInternal,
					fileAttributesExternal,
					offsetOfLocalHeader
				);

				entriesCentralDirectory.push(entry);
			}
			else if (signature == ZipFileCentralDirectoryEndRecord.Signature)
			{
				// It's an end of central directory record.
				var diskNumber = reader.readInteger16LE();
				var diskStart = reader.readInteger16LE();
				var recordsOnDisk = reader.readInteger16LE();
				var recordsTotal = reader.readInteger16LE();
				var sizeInBytes = reader.readInteger32LE();
				var offset = reader.readInteger32LE();
				var commentLength = reader.readInteger16LE();
				var comment = reader.readStringUTF8(commentLength);

				centralDirectoryEndRecord = new ZipFileCentralDirectoryEndRecord
				(
					diskNumber,
					diskStart,
					recordsOnDisk,
					recordsTotal,
					sizeInBytes,
					offset,
					comment
				);
			}
			else
			{
				throw "Unexpected format."
			}
		}

		var returnValue = new ZipFile(entriesLocal, entriesCentralDirectory, centralDirectoryEndRecord);
		return returnValue;
	}

	ZipFile.fromBytes_ReadTimeFromByteStream = function(reader)
	{
		// Based on the timestamp format for the FAT filesystem,
		// made popular by Microsoft's MS-DOS.

		var hours5Bits_Minutes6Bits_DualSeconds5Bits = reader.readInteger16LE();
		var hours = (hours5Bits_Minutes6Bits_DualSeconds5Bits >> 11);
		var minutes = (hours5Bits_Minutes6Bits_DualSeconds5Bits >> 5) & 0x3F;
		var secondsOver2 = hours5Bits_Minutes6Bits_DualSeconds5Bits & 0x1F;
		var seconds = secondsOver2 * 2;

		var years7Bits_Month4Bits_Days5Bits = reader.readInteger16LE();
		var fatFilesystemEpochYear = 1980;
		var year = (years7Bits_Month4Bits_Days5Bits >> 9) + fatFilesystemEpochYear;
		var month = ((years7Bits_Month4Bits_Days5Bits >> 5) & 0xF) - 1;
		var day = (years7Bits_Month4Bits_Days5Bits) & 0x1F;

		var returnValue = new Date(year, month, day, hours, minutes, seconds, 0);
		return returnValue;
	}

	// instance methods

	ZipFile.prototype.toStringJSON = function()
	{
		var indentSizeInSpaces = 2;
		var returnValue = JSON.stringify(this, null, indentSizeInSpaces);
		return returnValue;
	}
}

function ZipFileCentralDirectoryEntry
(
	signature,
	versionMadeBy,
	versionNeededToExtract,
	flags,
	compressionMethod,
	timeLastModified,
	crc32,
	sizeCompressedInBytes,
	sizeUncompressedInBytes,
	filename,
	extraFieldAsHexadecimal,
	fileComment,
	diskNumber,
	fileAttributesInternal,
	fileAttributesExternal,
	offsetOfLocalHeader
)
{
	// A "central directory" entry
	// is an expanded version of the "local" file entry header.

	this.signature = signature;
	this.versionMadeBy = versionMadeBy;
	this.versionNeededToExtract = versionNeededToExtract;
	this.flags = flags;
	this.compressionMethod = compressionMethod;
	this.timeLastModified = timeLastModified;
	this.crc32 = crc32; // "crc32" = "32-bit cyclical redundancy check" - Validates file not corrupt.
	this.sizeCompressedInBytes = sizeCompressedInBytes;
	this.sizeUncompressedInBytes = sizeUncompressedInBytes;
	this.filename = filename;
	this.extraFieldAsHexadecimal = extraFieldAsHexadecimal;
	this.fileComment = fileComment;
	this.diskNumber = diskNumber;
	this.fileAttributesInternal = fileAttributesInternal;
	this.fileAttributesExternal = fileAttributesExternal;
	this.offsetOfLocalHeader = offsetOfLocalHeader;	
}
{
	ZipFileCentralDirectoryEntry.Signature = 33639248;
}

function ZipFileCentralDirectoryEndRecord
(
	diskNumber,
	diskStart,
	recordsOnDisk,
	recordsTotal,
	sizeInBytes,
	offset,
	comment
)
{
	this.diskNumber = diskNumber;
	this.diskStart = diskStart;
	this.recordsOnDisk = recordsOnDisk;
	this.recordsTotal = recordsTotal;
	this.sizeInBytes = sizeInBytes;
	this.offset = offset;
	this.comment = comment;
}
{
	ZipFileCentralDirectoryEndRecord.Signature = 101010256;
}

function ZipFileLocalFileEntry(header, dataCompressedAsStringHexadecimal)
{
	this.header = header;
	this.dataCompressedAsStringHexadecimal = dataCompressedAsStringHexadecimal;
}

function ZipFileLocalFileHeader
(
	signature,
	versionNeededToExtract,
	flags,
	compressionMethod,
	timeLastModified,
	crc32,
	sizeCompressedInBytes,
	sizeUncompressedInBytes,
	filename,
	extraFieldAsHexadecimal
)
{
	// This is a "central directory" file header,
	// which is an expanded version of the "local" file header.

	this.signature = signature;
	this.versionNeededToExtract = versionNeededToExtract;
	this.flags = flags;
	this.compressionMethod = compressionMethod;
	this.timeLastModified = timeLastModified;
	this.crc32 = crc32;
	this.sizeCompressedInBytes = sizeCompressedInBytes;
	this.sizeUncompressedInBytes = sizeUncompressedInBytes;
	this.filename = filename;
	this.extraFieldAsHexadecimal = extraFieldAsHexadecimal;
}
{
	ZipFileLocalFileHeader.Signature = 67324752;
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , | 2 Comments

Another RPG Combat Engine in JavaScript

The code below implements a rudimentary turn-based combat engine for a Japanese-style role-playing game in JavaScript.

I have partially implemented a similar engine in a previous post, but I recently tried to update it and found it very difficult. I suppose I concentrated too much on how it looked and less on how it worked. So this time, I ignored the graphics completely to start with, and just used a simple all-text interface. I hope this will let me focus more on the architecture, which will therefore be cleaner and easier to expand and maintain.


<html>
<body>

<label><b>RPG Combat Engine</b></label>
<div id="divMain"></div>

<script type="text/javascript">

// main

function main()
{
	var actionDefns = ActionDefn.Instances()._All;
	
	var actionDefnNames = actionDefns.select("name");

	var party0 = new Party
	(
		"Party0",
		true, // isControlledByHuman
		// agents
		[
			new Agent("Amy", 10, actionDefnNames),
			new Agent("Bob", 10, actionDefnNames),
			new Agent("Cal", 10, actionDefnNames),
			new Agent("Deb", 10, actionDefnNames),
		]
	);
	
	var party1 = new Party
	(
		"Party1",
		false, // isControlledByHuman
		// agents
		[
			new Agent("William", 10, actionDefnNames),
			new Agent("Xavier", 10, actionDefnNames),
			new Agent("Yonni", 10, actionDefnNames),
			new Agent("Zane", 10, actionDefnNames),
		]
	);
	
	var encounter = new Encounter([party0, party1]);
	var encounterAsControl = encounter.toControl();
	var encounterAsDomElement = encounterAsControl.toDomElement();
	var divMain = document.getElementById("divMain");
	divMain.appendChild(encounterAsDomElement);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			this[key] = element;
		}
		return this;
	};
	
	Array.prototype.get = function()
	{
		return this;
	};
	
	Array.prototype.insertElementAt = function(element, index)
	{
		this.splice(index, 0, element);
		return this;
	}
		
	Array.prototype.remove = function(elementToRemove)
	{
		var indexToRemoveAt = this.indexOf(elementToRemove);
		if (indexToRemoveAt != null)
		{
			this.removeAt(indexToRemoveAt);
		}
		return this;
	}
	
	Array.prototype.removeAt = function(indexToRemoveAt)
	{
		this.splice(indexToRemoveAt, 1);
		return this;
	}
	
	Array.prototype.select = function(fieldName)
	{
		var returnValues = [];
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var fieldValue = this[fieldName];
			returnValues.push(fieldValue);
		}
		return returnValues;
	};
}

function BooleanExtensions()
{
	// extension class
}
{
	Boolean.prototype.get = function()
	{
		return (this == true); // To unwrap it.
	};
}

function StringExtensions()
{
	// extension class
}
{
	String.prototype.get = function()
	{
		return this;
	};
}

// classes

function Action(agentNameActor, defnName, targetName)
{
	this.agentNameActor = agentNameActor;
	this.defnName = defnName;
	this.targetName = targetName;
}
{
	Action.prototype.defn = function()
	{
		return ActionDefn.Instances()._All[this.defnName];
	}

	Action.prototype.isValid = function()
	{
		var returnValue = 
		(
			this.agentNameActor != null
			&& this.defnName != null
			&& this.targetName != null
		);
		return returnValue;
	};
	
	Action.prototype.performForEncounter = function(encounter)
	{
		this.effects = this.defn().performForEncounterAndAction(encounter, this);
	}

	Action.prototype.toString = function()
	{
		var returnValue =
			this.agentNameActor
			+ " used " + this.defnName
			+ " on " + this.targetName + ".  "
			+ (this.effects == null ? "" : this.effects.join("  "));
		return returnValue;
	};
}

function ActionDefn(name, targetTypeName, performForEncounterAndAction)
{
	this.name = name;
	this.targetTypeName = targetTypeName;
	this.performForEncounterAndAction = performForEncounterAndAction;
}
{
	ActionDefn.Instances = function()
	{
		if (ActionDefn._Instances == null)
		{
			ActionDefn._Instances = new ActionDefn_Instances();
		}
		
		return ActionDefn._Instances;
	};
	
	function ActionDefn_Instances()
	{
		var targetTypes = ActionTargetType.Instances()._All;
		var performTodo = function todo() { };
		
		this.Attack = new ActionDefn
		(
			"Attack", 
			targetTypes["Enemy"].name,
			function perform(encounter, action)
			{
				var targetName = action.targetName;			
				var partyTargeted = encounter.partyOther();
				var agentTargeted = partyTargeted.agents[targetName];
				var damage = 3;
				var effectDamage = new Effect("lost", damage, targetName);
				var effects = [ effectDamage ];
				agentTargeted.integrity -= damage;
				if (agentTargeted.integrity <= 0)
				{
					var effectDie = new Effect("died", null, targetName);
					effects.push(effectDie);
					partyTargeted.agents.remove(agentTargeted);
				}
				partyTargeted._control.invalidate();				
				return effects;
			}
		);
		this.Cower = new ActionDefn
		(
			"Cower", targetTypes["Self"].name, performTodo
		);
		this.Flee = new ActionDefn
		(
			"Flee", targetTypes["Self"].name, performTodo
		);
		this.Heal = new ActionDefn
		(
			"Heal", targetTypes["Ally"].name, performTodo
		);		
		this.Protect = new ActionDefn
		(
			"Protect", targetTypes["Ally"].name, performTodo
		);
		this.Wait = new ActionDefn
		(
			"Wait", targetTypes["None"].name, performTodo
		);
		
		this._All = 
		[ 
			this.Attack,
			//this.Cower,
			//this.Flee,
			//this.Heal,
			//this.Protect,
			//this.Wait
		].addLookups("name");
	}
	
	ActionDefn.prototype.targetType = function()
	{
		return ActionTargetType.Instances()._All[this.targetTypeName];
	}
}

function ActionTargetType(name, targetsGetForEncounter)
{
	this.name = name;
	this.targetsGetForEncounter = targetsGetForEncounter;
}
{
	ActionTargetType.Instances = function()
	{
		if (ActionTargetType._Instances == null)
		{
			ActionTargetType._Instances = new ActionTargetType_Instances();
		}
		
		return ActionTargetType._Instances;
	};
	
	function ActionTargetType_Instances()
	{
		this.Ally = new ActionTargetType
		(
			"Ally",
			function targetsGetForEncounter(encounter)
			{
				return encounter.partyCurrent().agents;
			}
		);
		this.Enemy = new ActionTargetType
		(
			"Enemy",
			function targetsGetForEncounter(encounter)
			{
				return encounter.partyOther().agents;
			}
		);
		this.None = new ActionTargetType
		(
			"None",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}
		);
		this.Other = new ActionTargetType
		(
			"Other",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);
		this.Party = new ActionTargetType
		(
			"Party",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);
		this.PartyAllies = new ActionTargetType
		(
			"AlliesAll",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);
		this.PartyEnemies = new ActionTargetType
		(
			"EnemiesAll",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);		
		this.Self = new ActionTargetType
		(
			"Self",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);
		
		this._All = 
		[
			this.Ally,
			this.Enemy,
			this.None,
			this.Other,
			this.Party,
			this.PartyAllies,
			this.PartyEnemies,
			this.Self
		].addLookups("name");
	}
}

function Agent(name, integrityMax, actionDefnNames)
{
	this.name = name;
	this.integrityMax = integrityMax;
	this.actionDefnNames = actionDefnNames;
	
	this.integrity = integrityMax;
	this.hasMovedThisTurn = false;
}
{
	Agent.prototype.toString = function()
	{
		return this.name + " (" + this.integrity + "/" + this.integrityMax + ")";
	}
}

function ControlBinding(context, get, set)
{
	this.context = context;
	this.get = (get == null ? this.getDefault : get).bind(this);
	this.set = (set == null ? this.setDefault : set).bind(this);
}
{
	ControlBinding.prototype.getDefault = function(binding)
	{
		return binding.context;
	};
	
	ControlBinding.prototype.setDefault = function(binding, value)
	{
		// Do nothing.
	};
}

function ControlBreak()
{}
{
	ControlBreak.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("br");
		}
		
		return this._domElement;
	};
}

function ControlButton(text, click, isEnabled)
{
	this.text = text;
	this.click = click;
	this.isEnabled = (isEnabled == null ? true : isEnabled);
}
{
	ControlButton.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("button");
			this._domElement.innerHTML = this.text;
			this._domElement.onclick = this.click;
		}
		
		var isEnabled = this.isEnabled.get();
		this._domElement.disabled = (isEnabled == false);
		
		return this._domElement;
	};
}

function ControlContainer(children)
{
	this.children = children;
	
	for (var i = 0; i < this.children.length; i++)
	{
		var child = this.children[i];
		child.parent = this;
	};
}
{
	ControlContainer.prototype.invalidate = function()
	{
		if (this.parent == null)
		{
			this.toDomElement();
		}
		else
		{
			this.parent.invalidate();
		}
	};

	ControlContainer.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("div");
			this._domElement.style.border = "1px solid";
			
			for (var i = 0; i < this.children.length; i++)
			{
				var child = this.children[i];
				var childAsDomElement = child.toDomElement();
				this._domElement.appendChild(childAsDomElement);
			}
		}
		
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.toDomElement();
		}
		
		return this._domElement;
	};
}

function ControlLabel(text)
{
	this.text = text;
}
{
	ControlLabel.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("label");
		}
		
		this._domElement.innerHTML = this.text.get();
		
		return this._domElement;
	};
}

function ControlList
(
	numberOfItemsVisible,
	bindingForItemText,
	bindingForItemValue,	
	bindingForSelectedValue,
	items
)
{
	this.numberOfItemsVisible = numberOfItemsVisible;
	this.bindingForItemText = bindingForItemText;
	this.bindingForItemValue = bindingForItemValue;
	this.bindingForSelectedValue = bindingForSelectedValue;
	this.items = items;
}
{
	ControlList.prototype.invalidate = function()
	{
		if (this.parent != null)
		{
			this.parent.invalidate();
		}
	};

	ControlList.prototype.toDomElement = function()
	{
		var control = this;
		
		if (this._domElement == null)
		{
			this._domElement = document.createElement("select");
			this._domElement.size = this.numberOfItemsVisible;
			this._domElement.onchange = function (event)
			{
				var selectedValue = event.target.value;
				control.bindingForSelectedValue.set(selectedValue);
				control.invalidate();
			};
		}
	
		this._domElement.innerHTML = "";
		var items = this.items.get();
		for (var i = 0; i < items.length; i++)
		{
			var item = items[i];
			var itemAsOption = document.createElement("option");
			this.bindingForItemText.context = item;
			var text = this.bindingForItemText.get();
			this.bindingForItemValue.context = item;
			var value = this.bindingForItemValue.get();
			itemAsOption.innerHTML = text;
			this._domElement.appendChild(itemAsOption);
		}
		
		this._domElement.value = this.bindingForSelectedValue.get();		
		
		return this._domElement;
	};
}

function ControlSpan(children, isVisible)
{
	this.children = children;
	this.isVisible = (isVisible == null ? true : isVisible);
	
	for (var i = 0; i < this.children.length; i++)
	{
		var child = this.children[i];
		child.parent = this;
	}
}
{
	ControlSpan.prototype.invalidate = function()
	{
		if (this.parent == null)
		{
			this.toDomElement();
		}
		else
		{
			this.parent.invalidate();
		}
	};

	ControlSpan.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("span");
			
			for (var i = 0; i < this.children.length; i++)
			{
				var child = this.children[i];
				var childAsDomElement = child.toDomElement();
				this._domElement.appendChild(childAsDomElement);
			}
		}
		
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.toDomElement();
		}
		
		var isVisible = this.isVisible.get();
		this._domElement.style.display = (isVisible ? "inline" : "none");
				
		return this._domElement;
	};
}

function Effect(defnName, magnitude, targetName)
{
	this.defnName = defnName;
	this.magnitude = magnitude;
	this.targetName = targetName;
}
{
	Effect.prototype.toString = function()
	{
		var returnValue = this.targetName + " " + this.defnName;
		if (this.magnitude != null)
		{
			returnValue += " " + this.magnitude;
		}
		returnValue += ".";
		return returnValue;
	};
}

function Encounter(parties)
{
	this.parties = parties;
	
	this.partyIndexCurrent = 0;
	this.agentIndexCurrent = 0;
	this.actionsSoFar = [];
	this.actionCurrent = new Action(this.agentCurrent().name);
}
{
	Encounter.prototype.actionDefnSelected = function()
	{
		var returnValue = ActionDefn.Instances()._All[this.actionCurrent.defnName];
		return returnValue;
	};
	
	Encounter.prototype.actionCurrentPerform = function()
	{
		var action = this.actionCurrent;
		if (action.isValid())
		{
			action.performForEncounter(this);
			
			this.actionsSoFar.insertElementAt(action, 0);
			
			this.agentCurrentAdvance();			
		}
	};
	
	Encounter.prototype.agentCurrent = function()
	{
		var returnValue = this.partyCurrent().agents[this.agentIndexCurrent];
		return returnValue;
	};
	
	Encounter.prototype.agentCurrentAdvance = function()
	{
		var partyCurrent = this.partyCurrent();
		var agents = partyCurrent.agents;
		this.agentIndexCurrent++;
		if (this.agentIndexCurrent >= agents.length)
		{
			this.partyIndexCurrent = 1 - this.partyIndexCurrent; 
			this.agentIndexCurrent = 0;
		}
		this.actionCurrent = new Action(this.agentCurrent().name);
		this._control.invalidate();
	};
			
	Encounter.prototype.agentsAll = function()
	{
		if (this._agentsAll == null)
		{
			this._agentsAll = this.parties[0].agents.concat(this.parties[1].agents);
		}
		
		return this._agentsAll;
	};
		
	Encounter.prototype.partyCurrent = function()
	{
		return this.parties[this.partyIndexCurrent];
	};
	
	Encounter.prototype.partyOther = function()
	{
		return this.parties[1 - this.partyIndexCurrent];
	};
		
	// controls

	Encounter.prototype.toControl = function()
	{
		if (this._control == null)
		{
			var controlsForParties = [];
			for (var i = 0; i < this.parties.length; i++)
			{
				var party = this.parties[i];
				var partyAsControl = party.toControl();
				controlsForParties.push(partyAsControl);
			}
			
			var containerActions = new ControlContainer
			([
				new ControlLabel("Next Action:"),
				new ControlBreak(),
				
				new ControlLabel
				(
					new ControlBinding
					(
						this, // context
						function get() { return this.context.agentCurrent().name; }
					)
				),
				new ControlLabel(" uses "),
				new ControlList
				(
					1, // numberOfItemsVisible
					// bindingGetForItemText
					new ControlBinding
					(
						null, function get() { return this.context.name; }
					),
					// bindingGetForItemValue
					new ControlBinding
					(
						null, function get() { return this.context.name; }
					),
					// bindingForSelectedValue
					new ControlBinding
					(
						this, // context 
						function get() { return this.context.actionCurrent.defnName; }, 
						function set(value) { return this.context.actionCurrent.defnName = value; } 						
					),				
					ActionDefn.Instances()._All
				),
				new ControlSpan
				( 
					[ 
						new ControlLabel(" on "),
						new ControlList
						(
							1, // numberOfItemsVisible,
							// bindingForItemText
							new ControlBinding
							(
								null, function get() { return this.context.name; }
							),
							// bindingForItemValue
							new ControlBinding
							(
								null, function get() { return this.context.name; }
							),
							// bindingForSelectedValue
							new ControlBinding
							(
								this, // context 
								function get() { return this.context.actionCurrent.targetName; }, 
								function set(value) { this.context.actionCurrent.targetName = value; }
							),
							// items
							new ControlBinding
							(
								this, // context
								function get()
								{
									var returnValues;
									var actionDefn = this.context.actionCurrent.defn();
									if (actionDefn == null)
									{
										returnValues = [];
									}
									else
									{
										var actionTargetType = actionDefn.targetType();
										returnValues = actionTargetType.targetsGetForEncounter(this.context);
									}
									return returnValues;
								}
							)
						),
					],
					// isVisible
					new ControlBinding
					(
						this, // context
						function get() 
						{ 
							var actionDefn = this.context.actionCurrent.defn();
							if (actionDefn == null)
							{
								return false;
							}
							var actionTargetType = actionDefn.targetType();
							var targets = actionTargetType.targetsGetForEncounter(this.context);
							var returnValue = (targets.length > 0);
							return returnValue;
						}
					) 
				),
				new ControlLabel(" "),
				
				new ControlButton
				(
					"Go",
					this.actionCurrentPerform.bind(this),
					// isEnabled
					new ControlBinding
					(
						this, // context
						function get() { return this.context.actionCurrent.isValid(); } 
					)
				),
				new ControlBreak(),
				
				new ControlLabel("Actions So Far:"),
				new ControlBreak(),
				new ControlList
				(
					3, // numberOfItemsVisible				
					// bindingForItemText
					new ControlBinding
					(
						null, function get() { return this.context.toString(); }
					),
					// bindingForItemValue
					new ControlBinding
					(
						null, // context 
						function get(binding) { return "todo"; }, 
					),									
					// bindingForSelectedValue
					new ControlBinding
					(
						null, // context 
						function get(binding) { return "todo"; }, 
					),				
					this.actionsSoFar
				)
			]);
		
			this._control = new ControlContainer
			(
				[
					controlsForParties[0],
					controlsForParties[1],
					containerActions
				]
			);
		}
		
		for (var i = 0; i < this.parties.length; i++)
		{
			var party = this.parties[i];
			party.toControl().toDomElement();
		}
	
		return this._control;
	};
}

function Party(name, isControlledByHuman, agents)
{
	this.name = name;
	this.isControlledByHuman = isControlledByHuman;
	this.agents = agents.addLookups("name");
}
{
	// controls
	
	Party.prototype.toControl = function()
	{
		if (this._control == null)
		{
			this._control = new ControlContainer
			([
				new ControlLabel(this.name),
				new ControlBreak(),
				new ControlList
				(
					this.agents.length, // numberOfItemsVisible
					// bindingForItemText
					new ControlBinding
					(
						null, // context 
						function get() { return this.context.toString(); }, 
					),
					// bindingForItemValue
					new ControlBinding
					(
						null, // context 
						function get() { return this.context.name; }, 
					),
					// bindingForSelectedValue
					new ControlBinding
					(
						this, // context 
						function get() { return this.context.agentNameSelected; }, 
						function set(value) { return this.context.agentNameSelected = value; }, 						
					),				
					this.agents // items
				),
				
				new ControlBreak(),
				new ControlLabel("Details:"),
				new ControlLabel
				(
					new ControlBinding
					(
						this, // context 
						function get() 
						{ 
							var returnValue = this.context.agentNameSelected;
							returnValue = (returnValue == null ? "[none]" : returnValue);
							return returnValue;
						}, 
						function set(value) { return this.context.agentNameSelected = value; }, 						
					)
				)
			]);
		}
		
		return this._control;
	};
}

// run
main();

</script>
</body>
</html>

Posted in Uncategorized | Tagged , , , | Leave a comment

A Text-Based SVG Editor in JavaScript

The JavaScript program below, when run, will present an interface that allows a user to edit a vector image in SVG format as text, and view the changes to the rendered image as they are made. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

This doesn’t really provide any real advantage over opening an SVG as a text editor and a web browser, but it does streamline the process somewhat. It also provides a convenient way to present a demonstration SVG that incorporates a lot of the format’s features in one place.

TextSVGEditor.png


<html>
<body>

<div id="divUI">
	<label><b>Text SVG Editor</b></label><br />
	<label>SVG as Text:</label>
	<button onclick="buttonNew_Clicked();">New</button>
	<button onclick="buttonDemo_Clicked();">Demo</button>	
	<br />
	<textarea id="textareaSVGAsText" cols="80" rows="20" onchange="textareaSVGAsText_Changed();">
	</textarea><br />
	<label>Rendered Image:</label><br />
	<div id="divImage"></div>
</div>

<script type="text/javascript">

function buttonDemo_Clicked()
{
	var textareaSVGAsText = document.getElementById("textareaSVGAsText");
	textareaSVGAsText.value = svgDemoAsText();
	textareaSVGAsText_Changed();
}

function svgDemoAsText()
{
	var svgAsTextLines =
	[
		"<?xml version='1.0' encoding='UTF-8' standalone='no'?>",
		"<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'>",
		"<g>",
		"",
		"<line x1='5' y1='90' x2='90' y2='5' stroke='black' stroke-width='4' stroke-linecap='butt'/>",
		"<line x1='10' y1='95' x2='95' y2='10' stroke='black' stroke-width='4' stroke-linecap='square' />",
		"",
		"<radialGradient id='radialGradient0' cx='50%' cy='50%' r='50%' fx='50%' fy='50%'>",
		"	<stop offset='0%' style='stop-color:blue;stop-opacity:1' />",
		"	<stop offset='90%' style='stop-color:blue;stop-opacity:1' />",
		"	<stop offset='100%' style='stop-color:white;stop-opacity:0' />",
		"</radialGradient>",
		"",
		"<rect x='10' y='10' width='80' height='80' fill='url(#radialGradient0)' />",
		"",
		"<linearGradient id='linearGradient0' x1='0%' y1='0%' x2='100%' y2='0%'>",
		"	<stop offset='0%' style='stop-color:red;stop-opacity:1' />",
		"	<stop offset='20%' style='stop-color:orange;stop-opacity:1' />",
		"	<stop offset='40%' style='stop-color:yellow;stop-opacity:1' />",
		"	<stop offset='60%' style='stop-color:green;stop-opacity:1' />",
		"	<stop offset='80%' style='stop-color:blue;stop-opacity:1' />",
		"	<stop offset='100%' style='stop-color:violet;stop-opacity:1' />",
		"</linearGradient>",
		"",
		"<ellipse cx='50' cy='50' rx='50' ry='30' fill='url(#linearGradient0)'/>",
		"",
		"<path fill='blue' stroke='black' d='M 0,0 20,0 20,20 0,20 z '/>",
		"",
		"<circle cx='90' cy='90' r='12' fill='red' stroke='black' />",
		"",
		"<polygon points='10,90 30,90 20,60' fill='green' stroke='none' />",
		"",
		"<path",
		"	style='stroke:#000000;fill:#00ff0080'",
		"	d='M 25,15 25,5 Q 90,10 95,65 M 95,65 100,65 90,75 80,65 85,65 Q 80,20 25,15'",
		"/>",
		"",
		"<text x='0' y='40' fill='black' transform='rotate(30 20,40)'>Texty text text.</text>",
		"",
		"<polyline points='5,5 50,20 50,80 95,95' fill='none' stroke='rgba(0,0,0,.5)' stroke-width='5' stroke-linecap='round' />",
		"",
		"</g>",
		"</svg>"	
	];
	var svgAsText = svgAsTextLines.join("\n");
	return svgAsText;
}

function buttonNew_Clicked()
{
	var textareaSVGAsText = document.getElementById("textareaSVGAsText");
	textareaSVGAsText.value = svgNewAsText();
	textareaSVGAsText_Changed();
}

function svgNewAsText()
{
	var svgAsTextLines =
	[
		"<?xml version='1.0' encoding='UTF-8' standalone='no'?>",
		"<svg xmlns='http://www.w3.org/2000/svg'>",
		"<g>",
		"</g>",
		"</svg>"
	];
	var svgAsText = svgAsTextLines.join("\n");
	return svgAsText;
}

function textareaSVGAsText_Changed()
{
	var textareaSVGAsText = document.getElementById("textareaSVGAsText");
	var svgAsText = textareaSVGAsText.value;
	var svgAsTextEncoded = encodeURIComponent(svgAsText);
	var svgAsDataURL = "data:image/svg+xml," + svgAsTextEncoded;
	var svgAsImgElement = document.createElement("img");
	svgAsImgElement.src = svgAsDataURL;
	var divImage = document.getElementById("divImage");
	divImage.innerHTML = "";
	divImage.appendChild(svgAsImgElement);
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , , | Leave a comment

An HTTP Echo Server in C# Using HttpListener

The C# code below implements a rudimentary HTTP server that simply echoes whatever is sent to it back to the client.

HttpEchoServer.png


using System;
using System.Net;
using System.IO;
using System.Text;

namespace HttpEchoServer
{
	public class HttpEchoServer
	{
		public static void Main()
		{
			string addressToListenOn = "http://localhost:1234/";
			Console.WriteLine("Starting echo server on " + addressToListenOn + "...");
			HttpListener listener = new HttpListener();
			listener.Prefixes.Add(addressToListenOn);
			listener.Start();
			while (true)
			{
				HttpListenerContext context = listener.GetContext();
				HttpListenerRequest request = context.Request;
				string stringToEcho = new StreamReader(request.InputStream).ReadToEnd();
				Console.WriteLine("Received: " + stringToEcho);
				HttpListenerResponse response = context.Response;
				byte[] responseBuffer = Encoding.UTF8.GetBytes(stringToEcho);
				response.ContentLength64 = responseBuffer.Length;
				Stream output = response.OutputStream;
				output.Write(responseBuffer, 0, responseBuffer.Length);
				output.Close();
				Console.WriteLine("Sent: " + stringToEcho);
			}
		}
	}
}

Posted in Uncategorized | Tagged , , , , , | Leave a comment

A Function Argument Type Validator in JavaScript

The JavaScript code below implements simple type checking on function arguments. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript, with the debugging console open so the log messages can be seen.

It still needs a little work. Notably, I need to make sure that the “this” value in the called functions maintain the appropriate values.


<html>
<body>
<script type="text/javascript">

// main

function main()
{
	var greeter = new Greeter();

	// The following calls should pass type check.

	console.log(greeter.personGreetOnBirthday("Marcia", 13));
	console.log(greeter.personGreetOnBirthday("Carol", 41, false));
	console.log(greeter.personGreetOnBirthday("Amos", 100, true));

	// The following calls should fail type check.

	var didFailAsExpected = false;	
	var validationWorkingMessage = "Failed type check as expected.";	
	var validationErrorMessage = "Should have failed type check!";
	
	try
	{
		greeter.personGreetOnBirthday("Marcia", "Marcia", "Marcia");
	}
	catch (error)
	{
		didFailAsExpected = true;
	}
	if (didFailAsExpected == true)
	{
		console.log(validationWorkingMessage);
	}
	else
	{
		console.log(validationErrorMessage);
	}
	
	try
	{
		greeter.personGreetOnBirthday("Carol", 41, "Yes I AM aging gracefully!");
	}
	catch (error)
	{
		didFailAsExpected = true;
	}
	if (didFailAsExpected == true)
	{
		console.log(validationWorkingMessage);
	}
	else
	{
		console.log(validationErrorMessage);
	}
	
	try
	{
		greeter.personGreetOnBirthday(true, "Amos", 100);
	}
	catch (error)
	{
		didFailAsExpected = true;
	}
	if (didFailAsExpected == true)
	{
		console.log(validationWorkingMessage);
	}
	else
	{
		console.log(validationErrorMessage);
	}
}

// extensions

function FunctionExtensions()
{
	// extension class
}
{
	Function.prototype.typesCheck = function(parameterTypeNames)
	{
		var functionToValidate = this;
		
		var functionWithTypeChecks = function()
		{
			for (var i = 0; i < arguments.length; i++)
			{
				var argument = arguments[i];

				var argumentTypeName = argument.constructor.name;
				var parameterTypeName = parameterTypeNames[i];
				
				if (argumentTypeName != parameterTypeName)
				{
					throw "Unexpected type.";
				}
			}
			
			var thisFromCall = null; // todo
			var returnValue = functionToValidate.apply(thisFromCall, arguments);
		
			return returnValue;
		}
		
		return functionWithTypeChecks;
	}
}

// classes

function Greeter()
{
	// Do nothing.
}
{
	Greeter.prototype.personGreetOnBirthday = function
	(
		personName, ageInYears, isAgingGracefully
	)
	{
		var message;
		
		if (isAgingGracefully == true || ageInYears < 21)
		{
			message = "Happy birthday";
		}
		else
		{
			message = "Hello";
		}
		
		message = message + ", " + personName + "!";
				
		return message;
		
	}.typesCheck(["String", "Number", "Boolean"]);
	
}

// run

main();

</script>
<body>
</html>

Posted in Uncategorized | Tagged , , , | Leave a comment

A CSV Compressor in JavaScript

Below is a simple program that can remove repeated values from a CSV, thus making the file smaller. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

I suppose it’s actually more of a “run-length suppressor” than a compressor, in that it only trims out value that repeat in subsequent rows. and will likely only be of much value in certain very limited contexts similar to the one I wrote it for. Nonetheless, I’m posting it here on the chance that someone might find it useful.


<html>
<body>

<div id="divUI">
	<label><b>CSV Repeated Value Compressor</b></label><br />
	<input type="file" onchange="inputFile_Changed(this);"></input><br />
	<textarea id="textareaData" cols="80" rows="25"></textarea><br />
	<button onclick="buttonCompress_Clicked();">Compress</button>
	<button onclick="buttonDecompress_Clicked();">Decompress</button>
	<button onclick="buttonSave_Clicked();">Save</button>
</div>

<script type="text/javascript">

// ui events

function buttonCompress_Clicked()
{
	var textareaData = document.getElementById("textareaData");
	var dataToCompress = textareaData.value;
	var csvCompressor = new CsvCompressor();
	var dataCompressed = csvCompressor.compress(dataToCompress);
	textareaData.value = dataCompressed;
}

function buttonSave_Clicked()
{
	var textareaData = document.getElementById("textareaData");

	var textToSave = textareaData.value;
	
	var textToSaveAsBlob = new Blob([textToSave], {type:"text/plain"});
	var textToSaveAsURL = window.URL.createObjectURL(textToSaveAsBlob);
	var fileNameToSaveAs = "Out.csv";

	var downloadLink = document.createElement("a");
	downloadLink.download = fileNameToSaveAs;
	downloadLink.innerHTML = "Download File";
	downloadLink.href = textToSaveAsURL;

	downloadLink.click();
}

function buttonDecompress_Clicked()
{
	var textareaData = document.getElementById("textareaData");
	var dataToDecompress = textareaData.value;
	var csvCompressor = new CsvCompressor();
	var dataDecompressed = csvCompressor.decompress(dataToDecompress);
	textareaData.value = dataDecompressed;
}

function inputFile_Changed(inputFile)
{
	var file = inputFile.files[0];
	if (file != null)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(event)
		{
			var fileContents = event.target.result;
			var textareaData = document.getElementById("textareaData");
			textareaData.value = fileContents;
		}
		fileReader.readAsText(file);
	}
}

// classes

function CsvCompressor()
{
	// Do nothing.
}
{
	CsvCompressor.Blank = "";
	CsvCompressor.Comma = ",";
	CsvCompressor.Newline = "\n";

	CsvCompressor.prototype.compress = function(dataToCompress)
	{
		var blank = CsvCompressor.Blank;
		var comma = CsvCompressor.Comma;
		var newline = CsvCompressor.Newline;
		
		var linesToCompress = dataToCompress.split(newline);
		var lineToCompress0 = linesToCompress[0];
		var valuesToCompressPrev = lineToCompress0.split(comma);
		var linesCompressed = [ lineToCompress0 ];
		for (var i = 1; i < linesToCompress.length; i++)
		{
			var lineToCompress = linesToCompress[i];
			var valuesToCompress = lineToCompress.split(comma);
			var valuesCompressed = [];
			for (var v = 0; v < valuesToCompress.length; v++)
			{
				var valueToCompress = valuesToCompress[v];
				var valueToCompressPrev = valuesToCompressPrev[v];
				var isValueSameAsPrev = (valueToCompress == valueToCompressPrev);
				var valueCompressed = (isValueSameAsPrev ? blank : valueToCompress);
				if (isValueSameAsPrev == false)
				{
					valuesToCompressPrev[v] = valueToCompress;
				}
				valuesCompressed.push(valueCompressed);
			}
			var lineCompressed = valuesCompressed.join(comma);
			linesCompressed.push(lineCompressed);
		}
		var dataCompressed = linesCompressed.join(newline);
		return dataCompressed;
	}
	
	CsvCompressor.prototype.decompress = function(dataToDecompress)
	{
		var blank = CsvCompressor.Blank;
		var comma = CsvCompressor.Comma;
		var newline = CsvCompressor.Newline;
		
		var linesToDecompress = dataToDecompress.split(newline);
		var lineToDecompress0 = linesToDecompress[0];
		var valuesToDecompressPrev = lineToDecompress0.split(comma);
		var linesDecompressed = [ lineToDecompress0 ];
		for (var i = 1; i < linesToDecompress.length; i++)
		{
			var lineToDecompress = linesToDecompress[i];
			var valuesToDecompress = lineToDecompress.split(comma);
			var valuesDecompressed = [];
			for (var v = 0; v < valuesToDecompress.length; v++)
			{
				var valueToDecompress = valuesToDecompress[v];
				var valueToDecompressPrev = valuesToDecompressPrev[v];
				var isValueBlank = (valueToDecompress == blank);
				var valueDecompressed = 
					(isValueBlank ? valueToDecompressPrev : valueToDecompress);
				if (isValueBlank == false)
				{
					valuesToDecompressPrev[v] = valueToDecompress;
				}
				valuesDecompressed.push(valueDecompressed);
			}
			var lineDecompressed = valuesDecompressed.join(comma);
			linesDecompressed.push(lineDecompressed);
		}
		var dataDecompressed = linesDecompressed.join(newline);
		return dataDecompressed;
	}
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , | Leave a comment