mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-28 10:06:08 -05:00
530 lines
19 KiB
JavaScript
530 lines
19 KiB
JavaScript
// This file is in JavaScript because we can't figure out how to get brunch to compile it bare.
|
|
|
|
if(typeof window !== 'undefined' || !self.importScripts)
|
|
throw "Attempt to load worker_world into main window instead of web worker.";
|
|
|
|
// Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
|
|
// This is here for running simuations in enviroments lacking function.bind (PhantomJS mostly)
|
|
if (!Function.prototype.bind) {
|
|
Function.prototype.bind = function (oThis) {
|
|
if (typeof this !== "function") {
|
|
// closest thing possible to the ECMAScript 5 internal IsCallable function
|
|
throw new TypeError("Function.prototype.bind (Shim) - target is not callable");
|
|
}
|
|
|
|
var aArgs = Array.prototype.slice.call(arguments, 1),
|
|
fToBind = this,
|
|
fNOP = function () {},
|
|
fBound = function () {
|
|
return fToBind.apply(this instanceof fNOP && oThis
|
|
? this
|
|
: oThis,
|
|
aArgs.concat(Array.prototype.slice.call(arguments)));
|
|
};
|
|
|
|
fNOP.prototype = this.prototype;
|
|
fBound.prototype = new fNOP();
|
|
|
|
return fBound;
|
|
};
|
|
}
|
|
|
|
// Assign global window so that Brunch's require (in world.js) can go into it
|
|
self.window = self;
|
|
self.workerID = "Worker";
|
|
|
|
self.logLimit = 200;
|
|
self.logsLogged = 0;
|
|
var console = {
|
|
log: function() {
|
|
if(self.logsLogged++ == self.logLimit)
|
|
self.postMessage({type: 'console-log', args: ["Log limit " + self.logLimit + " reached; shutting up."], id: self.workerID});
|
|
else if(self.logsLogged < self.logLimit) {
|
|
var args = [].slice.call(arguments);
|
|
for(var i = 0; i < args.length; ++i) {
|
|
if(args[i] && args[i].constructor) {
|
|
if(args[i].constructor.className === "Thang" || args[i].isComponent || args[i].isVector || args[i].isRectangle || args[i].isEllipse)
|
|
args[i] = args[i].toString();
|
|
}
|
|
}
|
|
try {
|
|
self.postMessage({type: 'console-log', args: args, id: self.workerID});
|
|
}
|
|
catch(error) {
|
|
try {
|
|
self.postMessage({type: 'console-log', args: ["Could not post log: " + args, error.toString(), error.stack, error.stackTrace], id: self.workerID});
|
|
}
|
|
catch(error2) {
|
|
self.postMessage({type: 'console-log', args: ["Wow, we had a serious problem trying to console.log something."]});
|
|
}
|
|
}
|
|
}
|
|
}}; // so that we don't crash when debugging statements happen
|
|
console.error = console.warn = console.info = console.debug = console.log;
|
|
self.console = console;
|
|
|
|
self.importScripts('/javascripts/lodash.js', '/javascripts/world.js', '/javascripts/aether.js');
|
|
var myImportScripts = importScripts;
|
|
|
|
var languagesImported = {};
|
|
var ensureLanguageImported = function(language) {
|
|
if (languagesImported[language]) return;
|
|
if (language === 'javascript') return; // Only has JSHint, but we don't need to lint here.
|
|
myImportScripts("/javascripts/app/vendor/aether-" + language + ".js");
|
|
languagesImported[language] = true;
|
|
};
|
|
|
|
var ensureLanguagesImportedFromUserCodeMap = function (userCodeMap) {
|
|
for (var thangID in userCodeMap)
|
|
for (var spellName in userCodeMap[thangID]) {
|
|
var language = userCodeMap[thangID][spellName].originalOptions.language;
|
|
ensureLanguageImported(language);
|
|
}
|
|
};
|
|
|
|
|
|
var restricted = ["XMLHttpRequest", "Worker"];
|
|
if (!self.navigator || !(self.navigator.userAgent.indexOf('MSIE') > 0) &&
|
|
!self.navigator.userAgent.match(/Trident.*rv\:11\./) &&
|
|
!self.navigator.userAgent.match(/Edge/)) {
|
|
// Can't restrict 'importScripts' in IE11, skip for all IE versions
|
|
restricted.push("importScripts");
|
|
}
|
|
for(var i = 0; i < restricted.length; ++i) {
|
|
// We could do way more from this: http://stackoverflow.com/questions/10653809/making-webworkers-a-safe-environment
|
|
Object.defineProperty(self, restricted[i], {
|
|
get: function() { throw new Error("Access to that global property is forbidden."); },
|
|
configurable: false
|
|
});
|
|
}
|
|
|
|
self.transferableSupported = function transferableSupported() {
|
|
if (typeof self._transferableSupported !== 'undefined') return self._transferableSupported;
|
|
// Not in IE, even in IE 11
|
|
try {
|
|
var ab = new ArrayBuffer(1);
|
|
worker.postMessage(ab, [ab]);
|
|
return self._transferableSupported = ab.byteLength == 0;
|
|
} catch(error) {
|
|
return self._transferableSupported = false;
|
|
}
|
|
return self._transferableSupported = false;
|
|
};
|
|
|
|
var World = self.require('lib/world/world');
|
|
var GoalManager = self.require('lib/world/GoalManager');
|
|
|
|
Aether.addGlobal('Vector', require('lib/world/vector'));
|
|
Aether.addGlobal('_', _);
|
|
|
|
var serializedClasses = {
|
|
"Thang": self.require('lib/world/thang'),
|
|
"Vector": self.require('lib/world/vector'),
|
|
"Rectangle": self.require('lib/world/rectangle'),
|
|
"Ellipse": self.require('lib/world/ellipse'),
|
|
"LineSegment": self.require('lib/world/line_segment')
|
|
};
|
|
self.currentUserCodeMapCopy = "";
|
|
self.currentDebugWorldFrame = 0;
|
|
|
|
self.stringifyValue = function(value, depth) {
|
|
var brackets, i, isArray, isObject, key, prefix, s, sep, size, v, values, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3;
|
|
if (!value || _.isString(value)) {
|
|
return value;
|
|
}
|
|
if (_.isFunction(value)) {
|
|
if (depth === 2) {
|
|
return void 0;
|
|
} else {
|
|
return "<Function>";
|
|
}
|
|
}
|
|
if (value === this.thang && depth) {
|
|
return "<this " + value.id + ">";
|
|
}
|
|
if (depth === 2) {
|
|
if (((_ref = value.constructor) != null ? _ref.className : void 0) === "Thang") {
|
|
value = "<" + (value.type || value.spriteName) + " - " + value.id + ", " + (value.pos ? value.pos.toString() : 'non-physical') + ">";
|
|
} else {
|
|
value = value.toString();
|
|
}
|
|
return value;
|
|
}
|
|
isArray = _.isArray(value);
|
|
isObject = _.isObject(value);
|
|
if (!(isArray || isObject)) {
|
|
return value.toString();
|
|
}
|
|
brackets = isArray ? ["[", "]"] : ["{", "}"];
|
|
size = _.size(value);
|
|
if (!size) {
|
|
return brackets.join("");
|
|
}
|
|
values = [];
|
|
if (isArray) {
|
|
for (_i = 0, _len = value.length; _i < _len; _i++) {
|
|
v = value[_i];
|
|
s = this.stringifyValue(v, depth + 1);
|
|
if (s !== void 0) {
|
|
values.push("" + s);
|
|
}
|
|
}
|
|
} else {
|
|
_ref2 = (_ref1 = value.apiProperties) != null ? _ref1 : _.keys(value);
|
|
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
|
|
key = _ref2[_j];
|
|
if (key[0] === "_") continue;
|
|
s = this.stringifyValue(value[key], depth + 1);
|
|
if (s !== void 0) {
|
|
values.push(key + ": " + s);
|
|
}
|
|
}
|
|
}
|
|
sep = '\n' + ((function() {
|
|
var _k, _results;
|
|
_results = [];
|
|
for (i = _k = 0; 0 <= depth ? _k < depth : _k > depth; i = 0 <= depth ? ++_k : --_k) {
|
|
_results.push(" ");
|
|
}
|
|
return _results;
|
|
})()).join('');
|
|
prefix = (_ref3 = value.constructor) != null ? _ref3.className : void 0;
|
|
if (isArray) {
|
|
if (prefix == null) {
|
|
prefix = "Array";
|
|
}
|
|
}
|
|
if (isObject) {
|
|
if (prefix == null) {
|
|
prefix = "Object";
|
|
}
|
|
}
|
|
prefix = prefix ? prefix + " " : "";
|
|
return "" + prefix + brackets[0] + sep + " " + (values.join(sep + ' ')) + sep + brackets[1];
|
|
};
|
|
|
|
|
|
self.retrieveValueFromFrame = function retrieveValueFromFrame(args) {
|
|
|
|
var retrieveProperty = function retrieveProperty(currentThangID, currentSpellID, variableChain)
|
|
{
|
|
var prop;
|
|
var value;
|
|
var keys = [];
|
|
for (var i = 0, len = variableChain.length; i < len; i++) {
|
|
prop = variableChain[i];
|
|
if (prop === "this")
|
|
{
|
|
value = self.debugWorld.thangMap[currentThangID];
|
|
|
|
}
|
|
else if (i === 0)
|
|
{
|
|
try
|
|
{
|
|
if (Aether.globals[prop])
|
|
{
|
|
value = Aether.globals[prop];
|
|
}
|
|
else
|
|
{
|
|
var flowStates = self.debugWorld.userCodeMap[currentThangID][currentSpellID].flow.states;
|
|
//we have to go to the second last flowState as we run the world for one additional frame
|
|
//to collect the flow
|
|
value = _.last(flowStates[flowStates.length - 1].statements).variables[prop];
|
|
}
|
|
}
|
|
catch (e)
|
|
{
|
|
value = undefined;
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
value = value[prop];
|
|
}
|
|
keys.push(prop);
|
|
if (!value) break;
|
|
var classOfValue;
|
|
if (classOfValue = serializedClasses[value.CN])
|
|
{
|
|
if (value.CN === "Thang")
|
|
{
|
|
var thang = self.debugWorld.thangMap[value.id];
|
|
value = thang || "<Thang " + value.id + " (non-existent)>";
|
|
}
|
|
else
|
|
{
|
|
value = classOfValue.deserializeFromAether(value);
|
|
}
|
|
}
|
|
}
|
|
var serializedProperty = {
|
|
"key": keys.join("."),
|
|
"value": self.stringifyValue(value,0)
|
|
};
|
|
self.postMessage({type: 'debug-value-return', serialized: serializedProperty});
|
|
};
|
|
self.enableFlowOnThangSpell(args.currentThangID, args.currentSpellID, args.userCodeMap);
|
|
self.setupDebugWorldToRunUntilFrame(args);
|
|
self.debugWorld.loadFrames(
|
|
retrieveProperty.bind({}, args.currentThangID, args.currentSpellID, args.variableChain),
|
|
self.onDebugWorldError,
|
|
self.onDebugWorldProgress,
|
|
false,
|
|
args.frame
|
|
);
|
|
};
|
|
|
|
self.enableFlowOnThangSpell = function (thangID, spellID, userCodeMap) {
|
|
try {
|
|
var options = userCodeMap[thangID][spellID].originalOptions;
|
|
if (options.includeFlow === true && options.noSerializationInFlow === true && options.noVariablesInFlow === false)
|
|
return;
|
|
else
|
|
{
|
|
options.includeFlow = true;
|
|
options.noSerializationInFlow = true;
|
|
options.noVariablesInFlow = false;
|
|
var temporaryAether = Aether.deserialize(userCodeMap[thangID][spellID]);
|
|
temporaryAether.transpile(temporaryAether.raw);
|
|
userCodeMap[thangID][spellID] = temporaryAether.serialize();
|
|
}
|
|
|
|
}
|
|
catch (error) {
|
|
console.log("Debug error enabling flow on", thangID, spellID + ":", error.toString() + "\n" + error.stack || error.stackTrace);
|
|
}
|
|
};
|
|
|
|
self.setupDebugWorldToRunUntilFrame = function (args) {
|
|
self.debugPostedErrors = {};
|
|
self.debugt0 = new Date();
|
|
self.logsLogged = 0;
|
|
|
|
ensureLanguagesImportedFromUserCodeMap(args.userCodeMap);
|
|
var stringifiedUserCodeMap = JSON.stringify(args.userCodeMap);
|
|
var userCodeMapHasChanged = ! _.isEqual(self.currentUserCodeMapCopy, stringifiedUserCodeMap);
|
|
self.currentUserCodeMapCopy = stringifiedUserCodeMap;
|
|
if (!self.debugWorld || userCodeMapHasChanged || args.frame < self.currentDebugWorldFrame) {
|
|
try {
|
|
self.debugWorld = new World(args.userCodeMap);
|
|
self.debugWorld.levelSessionIDs = args.levelSessionIDs;
|
|
self.debugWorld.submissionCount = args.submissionCount;
|
|
self.debugWorld.flagHistory = args.flagHistory;
|
|
self.debugWorld.difficulty = args.difficulty;
|
|
if (args.level)
|
|
self.debugWorld.loadFromLevel(args.level, true);
|
|
self.debugWorld.debugging = true;
|
|
self.debugGoalManager = new GoalManager(self.debugWorld);
|
|
self.debugGoalManager.setGoals(args.goals);
|
|
self.debugGoalManager.setCode(args.userCodeMap);
|
|
self.debugGoalManager.worldGenerationWillBegin();
|
|
self.debugWorld.setGoalManager(self.debugGoalManager);
|
|
}
|
|
catch (error) {
|
|
self.onDebugWorldError(error);
|
|
return;
|
|
}
|
|
Math.random = self.debugWorld.rand.randf; // so user code is predictable
|
|
Aether.replaceBuiltin("Math", Math);
|
|
var replacedLoDash = _.runInContext(self);
|
|
for(var key in replacedLoDash)
|
|
_[key] = replacedLoDash[key];
|
|
}
|
|
self.debugWorld.totalFrames = args.frame; //hack to work around error checking
|
|
self.currentDebugWorldFrame = args.frame;
|
|
};
|
|
|
|
|
|
self.onDebugWorldLoaded = function onDebugWorldLoaded() {
|
|
self.postMessage({type: 'debug-world-loaded'});
|
|
};
|
|
|
|
self.onDebugWorldError = function onDebugWorldError(error) {
|
|
|
|
if(!error.isUserCodeProblem) {
|
|
console.log("Debug Non-UserCodeError:", error.toString() + "\n" + error.stack || error.stackTrace);
|
|
}
|
|
return true;
|
|
};
|
|
|
|
self.onDebugWorldProgress = function onDebugWorldProgress(progress) {
|
|
self.postMessage({type: 'debug-world-load-progress-changed', progress: progress});
|
|
};
|
|
|
|
self.debugAbort = function () {
|
|
if(self.debugWorld) {
|
|
self.debugWorld.abort();
|
|
self.debugWorld.destroy();
|
|
self.debugWorld = null;
|
|
}
|
|
self.postMessage({type: 'debug-abort'});
|
|
};
|
|
|
|
self.runWorld = function runWorld(args) {
|
|
self.postedErrors = {};
|
|
self.t0 = new Date();
|
|
self.logsLogged = 0;
|
|
|
|
try {
|
|
ensureLanguagesImportedFromUserCodeMap(args.userCodeMap);
|
|
self.world = new World(args.userCodeMap);
|
|
self.world.levelSessionIDs = args.levelSessionIDs;
|
|
self.world.submissionCount = args.submissionCount;
|
|
self.world.flagHistory = args.flagHistory || [];
|
|
self.world.difficulty = args.difficulty || 0;
|
|
if(args.level)
|
|
self.world.loadFromLevel(args.level, true);
|
|
self.world.preloading = args.preload;
|
|
self.world.headless = args.headless;
|
|
self.world.realTime = args.realTime;
|
|
self.goalManager = new GoalManager(self.world);
|
|
self.goalManager.setGoals(args.goals);
|
|
self.goalManager.setCode(args.userCodeMap);
|
|
self.goalManager.worldGenerationWillBegin();
|
|
self.world.setGoalManager(self.goalManager);
|
|
}
|
|
catch (error) {
|
|
self.onWorldError(error);
|
|
return;
|
|
}
|
|
Math.random = self.world.rand.randf; // so user code is predictable
|
|
Aether.replaceBuiltin("Math", Math);
|
|
var replacedLoDash = _.runInContext(self);
|
|
for(var key in replacedLoDash)
|
|
_[key] = replacedLoDash[key];
|
|
self.postMessage({type: 'start-load-frames'});
|
|
self.world.loadFrames(self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress, self.onWorldPreloaded);
|
|
};
|
|
|
|
self.serializeFramesSoFar = function serializeFramesSoFar() {
|
|
if(!self.world) return; // We probably got this message late, after delivering the world.
|
|
if(self.world.framesSerializedSoFar == self.world.frames.length) return;
|
|
self.onWorldLoaded();
|
|
self.world.framesSerializedSoFar = self.world.frames.length;
|
|
};
|
|
|
|
self.onWorldLoaded = function onWorldLoaded() {
|
|
if(self.world.framesSerializedSoFar == self.world.frames.length) return;
|
|
if(self.world.ended)
|
|
self.goalManager.worldGenerationEnded();
|
|
var goalStates = self.goalManager.getGoalStates();
|
|
var overallStatus = self.goalManager.checkOverallStatus();
|
|
if(self.world.ended)
|
|
self.postMessage({type: 'end-load-frames', goalStates: goalStates, overallStatus: overallStatus});
|
|
var t1 = new Date();
|
|
var diff = t1 - self.t0;
|
|
if(self.world.headless)
|
|
return console.log('Headless simulation completed in ' + diff + 'ms.');
|
|
|
|
var worldEnded = self.world.ended;
|
|
var totalFrames = self.world.totalFrames;
|
|
var transferableSupported = self.transferableSupported();
|
|
try {
|
|
var serialized = self.world.serialize();
|
|
}
|
|
catch(error) {
|
|
console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace);
|
|
self.destroyWorld();
|
|
return;
|
|
}
|
|
//self.serialized = serialized; // Testing peak memory usage
|
|
//return; // Testing peak memory usage
|
|
if(worldEnded)
|
|
// Make sure we clean up memory as soon as possible, since we just used the most ever and don't want to crash.
|
|
self.destroyWorld();
|
|
|
|
var t2 = new Date();
|
|
//console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects);
|
|
var messageType = worldEnded ? 'new-world' : 'some-frames-serialized';
|
|
try {
|
|
var message = {type: messageType, serialized: serialized.serializedWorld, goalStates: goalStates, startFrame: serialized.startFrame, endFrame: serialized.endFrame};
|
|
if(transferableSupported)
|
|
self.postMessage(message, serialized.transferableObjects);
|
|
else
|
|
self.postMessage(message);
|
|
}
|
|
catch(error) {
|
|
console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace);
|
|
}
|
|
|
|
if(worldEnded) {
|
|
var t3 = new Date();
|
|
console.log("And it was so: (" + (diff / totalFrames).toFixed(3) + "ms per frame,", totalFrames, "frames)\nSimulation :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery :", (t3 - t2) + "ms");
|
|
}
|
|
};
|
|
|
|
self.destroyWorld = function destroyWorld() {
|
|
self.world.goalManager.destroy();
|
|
self.world.destroy();
|
|
self.world = null;
|
|
};
|
|
|
|
self.onWorldPreloaded = function onWorldPreloaded() {
|
|
self.goalManager.worldGenerationEnded();
|
|
var goalStates = self.goalManager.getGoalStates();
|
|
var overallStatus = self.goalManager.checkOverallStatus();
|
|
self.postMessage({type: 'end-preload-frames', goalStates: goalStates, overallStatus: overallStatus});
|
|
};
|
|
|
|
self.onWorldError = function onWorldError(error) {
|
|
if(error.isUserCodeProblem) {
|
|
var errorKey = error.userInfo.key;
|
|
if(!errorKey || !self.postedErrors[errorKey]) {
|
|
self.postMessage({type: 'user-code-problem', problem: error});
|
|
self.postedErrors[errorKey] = error;
|
|
}
|
|
}
|
|
else {
|
|
console.log("Non-UserCodeError:", error.toString() + "\n" + error.stack || error.stackTrace);
|
|
self.postMessage({type: 'non-user-code-problem', problem: {message: error.toString()}});
|
|
return false;
|
|
}
|
|
/* We don't actually have the recoverable property any more; hmm
|
|
if(!error.recoverable) {
|
|
self.abort();
|
|
return false;
|
|
}
|
|
*/
|
|
return true;
|
|
};
|
|
|
|
self.onWorldLoadProgress = function onWorldLoadProgress(progress) {
|
|
self.postMessage({type: 'world-load-progress-changed', progress: progress});
|
|
};
|
|
|
|
self.abort = function abort() {
|
|
if(self.world) {
|
|
self.world.abort();
|
|
self.world.goalManager.destroy();
|
|
self.world.destroy();
|
|
self.world = null;
|
|
}
|
|
self.postMessage({type: 'abort'});
|
|
};
|
|
|
|
self.reportIn = function reportIn() {
|
|
self.postMessage({type: 'report-in'});
|
|
};
|
|
|
|
self.finalizePreload = function finalizePreload() {
|
|
self.world.finalizePreload(self.onWorldLoaded);
|
|
};
|
|
|
|
self.addFlagEvent = function addFlagEvent(flagEvent) {
|
|
if(!self.world) return;
|
|
self.world.addFlagEvent(flagEvent);
|
|
};
|
|
|
|
self.stopRealTimePlayback = function stopRealTimePlayback() {
|
|
if(!self.world) return;
|
|
self.world.realTime = false;
|
|
};
|
|
|
|
self.addEventListener('message', function(event) {
|
|
self[event.data.func](event.data.args);
|
|
});
|
|
|
|
self.postMessage({type: 'worker-initialized'});
|