code:
"use strict";
    Game_.prototype.castMap = function(s) {
        var s = Data.Spell[s];
        Game.mana -= s.cost;
        s.castMap();
    }

    var Spell = function(name, desc, cost) {
            this.name = name;
            this.description = desc;
            this.cost = cost;
            this.targets = [ 'person' ];
            Data.addSpell(this);
    }

    Spell.prototype.Target = function(t) {
            if (!isArray(this.targets)) {
                    ToText(this.name + " is broken
");
                    return false;
            }
            return this.targets.indexOf(t) > -1;
    }

    Spell.prototype.cast = function() {
            ToText("You cast " + this.name + ", but it fizzles.
");
    }

    Spell.prototype.CastOn = function(girl) {
            ToText("You cast " + this.name + " on " +girl.name +" , but it fizzles.
");
    }

    Spell.prototype.castEncounter = function() {
        ToText("You cast " + this.name + ", but nothing happens.
");
    }

    Spell.prototype.castMap = function() {}

    /* Add spells */

    var sp;
    sp = new Spell('Mind Read', 'Allows you to see into person′s mind and get accurate information on them.', 3);
    sp.CastOn = function(girl) {
        girl.trait_known = 1;
        ToText("You focus intently on " + girl.name + ", peering into her soul.
She possess these skills:

 ");
        this.skills(girl);

        this.attr(girl);

        this.trait(girl);
    }

    sp.skills = function(girl) {
        if (girl.skill.combat > 0) {
            ToText("Combat: " + Data.skill_adjective[girl.skill.combat] + '' + "
");
        }
        if (girl.skill.bodycontrol > 0) {
            ToText("Body Control: " + Data.skill_adjective[girl.skill.bodycontrol] + '' + "
");
        }
        if (girl.skill.survival > 0) {
            ToText("Survival: " + Data.skill_adjective[girl.skill.survival] + '' + "
");
        }
        if (girl.skill.management > 0) {
            ToText("Management: " + Data.skill_adjective[girl.skill.management] + '' + "
");
        }
        if (girl.skill.service > 0) {
            ToText("Service: " + Data.skill_adjective[girl.skill.service] + '' + "
");
        }
        if (girl.skill.allure > 0) {
            ToText("Allure: " + Data.skill_adjective[girl.skill.allure] + '' + "
");
        }
        if (girl.skill.sex > 0) {
            ToText("Sexual Proficiency: " + Data.skill_adjective[girl.skill.sex] + '' + "
");
        }
        if (girl.skill.magicarts > 0) {
            ToText("Magic Art: " + Data.skill_adjective[girl.skill.magicarts] + '' + "
");
        };
    }

    sp.attr = function(girl) {
        ToText("

Loyalty: " + girl.loyalty + '' + "/100.
Obedience: " + girl.obedience + '' + "/100. Corruption " + girl.corruption + '' + "/100. Stress: " + girl.stress + '' + ". Lust: " + girl.lust + '' + ". Toxicity: " + girl.body.toxicity + '' + ".

");
    }

    sp.trait = function(girl) {
        if (girl.trait != 0) {
            ToText(" Sexually, she's " + Data.sex_trait[girl.trait] + '' + ". ");
        }
        if (girl.trait2 != 0) {
            ToText(" Mentally, she's " + Data.mental_trait[girl.trait2] + '' + ". ");
        }
        ToText("

");
    }

    sp.combat = function() {
        if (Game.combat.health <= 5) {
            ToText(" Low ");
        } else if (Game.combat.health <= 10) {
            ToText(" Medium ");
        } else if (Game.combat.health <= 15) {
            ToText(" High ");
        } else {
            ToText(" Extreme. ");
        }
        ToText("

Danger for " + Game.Girls[Game.companion].name + '' + " — ");
        if (Game.combat.power <= 5) {
            ToText(" Negligible ");
        } else if (Game.combat.power <= 10) {
            ToText(" Low ");
        } else if (Game.combat.power <= 15) {
            ToText(" Considerable ");
        } else if (Game.combat.power <= 20) {
            ToText(" High ");
        } else {
            ToText(" Extreme ");
        }
        ToText("

");
        if (Game.combat.procs.length >= 1) {
            ToText(" Special Skills — ");
            Game.tmp0 = 0;
            for (var loop_asm22 = 1; loop_asm22 <= Game.combat.procs.length; loop_asm22++) {
                ToText(" " + Data.enemy_action[Game.combat.procs[Game.tmp0]] + '' + " ");
                Game.tmp0++;
            }
        }
        ToText("

");
    }

    sp.castEncounter = function() {
        ToText("Toughness — ");

        this.combat();

        if (Game.combat.iscapturable == true) {
            ToText(" Willpower — " + Data.willpower_adjective[Game.newgirl.attr.willpower] + "
");

            this.skills(Game.newgirl);

            this.trait(Game.newgirl);
        }
    };

    sp.targets.push('encounter');

    sp = new Spell('Sedation', 'Creates a calming aura that may help the target relax.', 7);
    sp.CastOn = function(girl) {
        ToText("You cast " + Data.Spell[Game.tmp2].name + '' + " on " + girl.name + '' + ". This makes her slightly more obedient and relieves some of the stress. ");
        girl.obedience = Math.min(girl.obedience + 10, 100);
        girl.stress = Math.max(girl.stress - 20, 0);
    }
    sp.CastCombat = function(girl) {
    }
    sp.targets.push('combat');

    sp = new Spell('Heal', 'Heals physical wounds.', 6);
    sp.CastOn = function(girl) {
        ToText("You use your power to heal " + girl.name + '' + ". Her wounds close up. ");
        ToText(" She seems to be grateful for your care. ");
        girl.health = Math.min(girl.health + 20, 100);
        girl.obedience = Math.min(girl.obedience + 10, 100);
        girl.loyalty = Math.min(girl.loyalty + 5, 100);
    }
    sp.targets.push('combat');

    sp = new Spell('Dream', 'Puts target into deep, restful sleep.', 8);
    sp.CastOn = function(girl) {
        ToText(girl.name + " quickly loses her consciousness and seems to be unable to wake up. You decide to let her rest for this day. ");
        girl.busy = 1;
        girl.health = Math.min(girl.health + 10, 100);
        girl.stress = Math.max(girl.stress - 40, 0);
    }

    sp = new Spell('Entrancement', 'Makes target more susceptible to suggestions and easier to acquire various kinks.', 15);
    sp.CastOn = function(girl) {
        ToText("You cast " + this.name + " on " + girl.name + ". ");
        if (girl.attr.willpower < 5 || girl.loyalty >= 50) {
            ToText(" Light gradually fades from her eyes, and her gaze becomes downcast. She seems ready to accept whatever you tell her. ");
            if (girl.backstory == Data.backstory1[1]) {
                ToText(" Now, what suggestion should you implant her with?Tell her she has always been with youInput custom memory", false);
            }
            girl.spell_effect = 1;
        } else {
            ToText(" Her will is too strong or she's not trusting enough for spell to take effect. ");
        }
    }

    sp = new Spell('Fear', 'Fills the target with primal terror Makes for a good punishment.', 8);
    sp.CastOn = function(girl) {
        ToText("You grab hold of " + girl.name + ""s shoulders and hold her gaze. At first, she’s calm, but the longer you stare into her eyes, the more she trembles in fear. Soon, panic takes over her stare. She better understands not to resist you, but her stress and courage worsens. " + '' + " ");
        girl.obedience = Math.min(girl.obedience + 40, 100);
        girl.stress += 15;
        girl.attr.courage = Math.max(girl.attr.courage - Math.tossDice(2, 5), 1);
        girl.WillRecounter();
    }

    sp = new Spell('Domination', 'Attempts to overwhelm the target′s mind and instill unwavering obedience. May cause irreversible mental trauma.', 25);
    sp.CastOn = function(girl) {
        ToText(girl.name + " shudders as your Domination spell hits her. ");
        if (girl.attr.willpower < 5) {
            if (Math.tossCoin(0.7) || girl.spell_effect == 1) {
                girl.obedience = 100;
                girl.loyalty = Math.min(girl.loyalty + 25, 100);
                ToText(" As she looks into your eyes, you can feel how any inner resistance left in her is slipping away. Her mind completely taken over by your influence and she listens to you with utter devotion. ");
            } else {
                ToText("  Her mind rebounds and is damaged by the spell. " + '' + " ");
                girl.attr.wit = Math.round(Math.max(girl.attr.wit - girl.attr.wit / 5, 1));
                girl.attr.confidence = Math.max(girl.attr.confidence - Math.round(girl.attr.confidence / 5), 1);
                girl.attr.charm = Math.max(girl.attr.charm - Math.round(girl.attr.charm / 5), 1);
                girl.WillRecounter();
            }
        } else {
            ToText(" Her current will is too great for this spell to affect her. ");
        }
    }

    sp = new Spell('Summon Tentacle', 'Summons naughty tentacles from the otherworld for a short time.', 10);
    sp.CastOn = function(girl) {
        ToText("You intone the ancient words at " + girl.name + ". As you finish your chanting, multiple small portals open around you. Tentacles quickly begin pouring out, and begin wrapping themselves around " + girl.name + '' + "'s body. There’s little she can do to resist, and soon enough she’s completely at their mercy.
");
        if (girl.trait != 3) {
            ToText("She struggles in vain to break free, but the tentacles easily overpower her, and she is quickly forced to orgasm. ");
            girl.stress += Math.round((100 - girl.lust) / 10) + 10;
            girl.attr.courage = Math.max(girl.attr.courage - Math.tossDice(5, 10), 1);
            if (Math.tossCoin(0.25) || girl.spell_effect == 1) {
                girl.trait = 3;
                ToText(" She became deviant." + '' + " ");
            }
            girl.WillRecounter();
        } else {
            ToText(" She seems to be reveling at the odd situation you’ve put her in, not even trying to hide the pleasure she’s taking from being raped by tentacles in front of you. She’s brought to an intense orgasm as her assailants mindlessly pound away.");
        }
        if (girl.body.pussy.virgin.value) {
            ToText(" She lets out a shriek as the largest tentacle finds its way to her pussy and tears through her hymen, claiming her virginity. ");
            girl.body.pussy.virgin = false;
            girl.health = girl.health - 20;
        }
        Game.tojail_or_race_description = 0;
        var mana = 2;
        ToText("

");
        mana += girl.orgasm();
        if (Game.magic_ring == 1) {
            Game.mana += mana;
            ToText(" Your ring fills with power (" + mana + " mana). ");
        }
    }

    sp = new Spell('Daze', 'Briefly distracts the target. Useful in freeing your companion from grapples and restraints.', 5);
    sp.targets = ['combat'];

    sp = new Spell('Mutate', 'Forces random mutation onto subject. Results will vary.', 4);
    sp.CastOn = function(girl) {
        ToText("You weave sickening green threads of magic around " + girl.name + ".

");
        girl.mutate();
    }

    sp = new Spell('Mind Blast', 'Sends negative energy onto the target. Deals severe damage. May damage target′s mind.', 20);
    sp.targets = ['combat'];

    sp = new Spell('Rejuvenation', 'A Strong spell, which heals and relives target′s stress. Builds some toxicity.', 15);
    sp.targets.push('combat');
    sp.CastOn = function(girl) {
        ToText("You cast " + this.name + " on " + girl.name + '' + ". Her wounds close up and she looks more relaxed.");
        girl.stress = Math.max(girl.stress - 50, 0);
        girl.health = Math.min(girl.health + 65, 100);
        girl.body.toxicity = Math.max(girl.body.toxicity + Math.tossDice(5, 15), 100);
    }

    sp = new Spell('Guidance', 'A utility spell, allowing to quickly pass by areas without getting into fights.', 10);
    sp.targets = [ 'map' ];
    sp.castMap = function() {
        var z = Data.Zone(Game.mappos);
        Game.zone_travelled = z.size;
        PrintLocation('mapencs');
        // FIXME run a popup here to explain what happened.
    }

    sp = new Spell('Coercion', 'The victim will be more open to sexual advances', 5);
    sp.CastOn = function(girl) {
        ToText("You cast Coercion on " + girl.name + ", making her more compliant with your sexual demands.
She seems unhappy with this.");
        girl.spell_effect = 2;
        girl.stress = Math.min(100, girl.stress + Math.tossDice(5, 11));
        girl.loyalty = Math.max(0, girl.loyalty - 5);
    }

repo_name: spdevel/sfp

license: unknown


code:
<!DOCTYPE html>

  <meta name="generator" content="HTML Tidy for HTML5 for Linux version 5.2.0">
  <title>Strive for Power - Web edition</title>
  <meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
  <meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'>
  <meta name='mobile-web-app-capable' content='yes'>
  <meta name='apple-mobile-web-app-capable' content='yes'>
  <meta name='apple-mobile-web-app-status-bar-style' content='black'>
  <meta http-equiv='X-UA-Compatible' content='IE=edge'>
  <link rel="stylesheet" type='text/css' href="style.css">

  <audio id='musicAudio' preload="" loop="" style='display:none;'></audio>

  This game requires JavaScript. Please enable it in your browser.


code:
"use strict";

    var Game_ = function(orig) {
        this.Girls = [];
        this.baby = [];
        this.mission = {}
        this.player = Data.characters.player();

        this.newgirl = undefined;
        this.combat = undefined; // force errors if not initialized
        this.requested = undefined;

        this.roles = {}
        this.roles.manager = -1;
        this.roles.jailer = -1;
        this.companion = -1;
        this.roles.labassistant = -1;

        /* Default starting info */
        this.energy = 5;
        this.gold = 500;
        this.food = 200;
        this.health = 100;
        this.mana = 5;
        this.guildrank = 0;
        this.spells_known = {}
        this.library_level = 0;
        this.lab_level = 0;

        this.settings = {};
        this.settings.futanari = 1;
        this.settings.futa_balls = 1;
        this.settings.furry = 1;
        this.settings.loli = 0;
        this.settings.flavor_image = 1;
        this.settings.tutorial = 0;
        this.settings.hide_description = 0;
        this.settings.per_race_images = 0;
        this.settings.textpopulation = 0;
        this.settings.combat_graph_length = 20;

        this.cum_inside = 0;

        this.inventory = {};

        var keys = Object.keys(Data.item);
        for (var i = 0; i < keys.length; i++)
            this.inventory[keys[i]] = 0;

        /* known things */

        this.setup = undefined;
        this.tmp0 = undefined; // var0
        this.tmp1 = undefined; // selected
        this.tmp2 = undefined; // var2
        this.tmp3 = undefined; // var13
        this.tmp5 = undefined; // var5
        this.tmp6 = undefined; // var176

        this.revert = {};
        this.revert.food = 0;
        this.revert.gold = 0;
        this.revert.mana = 0;

        this.days_elapsed = 1; // days passed
        this.dates_available = 2;

        this.restock = {};
        this.restock.slaveguild = 0;
        this.restock.sebastian = 0;
        Object.preventExtensions(this.restock);

        this.companion_sexact = 0;

        this.headgirl_orders = 0;  // headgirl mission
        this.sebastian_request = 0; // sebastian race request

        this.dates_available = 2; // dates left today

        this.hide_tutorials = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];

        this.alchemy_level = 0;
        this.gold_change = 0;
        this.tojail_or_race_description = 0;
        this.magic_ring = 1;
        this.magic_knife = 0;
        this.reservoirs = 0;
        this.personal_rooms = 1;
        this.my_bed_capacity = 2;
        this.sandbox_mode = 0;

        this.jail_capacity = 3;
        this.max_residents = 6;
        this.last_location = undefined;

        this.slaversguild_firstvisit = 0;
        this.patron_beg = 0;

        this.mission.brothel = 0;
        this.mission.cali = 0;
        this.mission.dolin = 0;
        this.mission.farm = 0;
        this.mission.lis = 0;
        this.mission.magequest = 0;
        this.mission.plant = 0;
        this.mission.street_urchin = 1;
        this.mission.slaveguild = 0;
        Object.preventExtensions(this.mission);

        this.learned_refined_brand = 0;

        this.builders_unlocked = 0;

        this.snails_unlocked = 0;
        this.total_snails = 0; // total snails

        this.undercity_cellar = 0;
        this.undercity_unlocked = 0;

        this.guildrank = 0;

        this.roles.labassistant = -1;
        this.roles.jailer = -1;
        this.roles.manager = -1;
        this.roles.headgirl = -1;
        this.roles.cook = -1;
        this.roles.nurse = -1;
        Object.preventExtensions(this.roles);

        this.portals = [];

        this.zone_size = undefined;
        this.zone_travelled = 0;

        this.mappos = undefined;
        this.encounter = undefined;

        this.books_read = [];

        Object.preventExtensions(this);

        if (orig !== undefined) {
            var keys = Object.keys(this);
            for (var i = 0; i < keys.length; i++) {
                if (!this.hasOwnProperty(keys[i]) || !(keys[i] in orig))
                    continue;
                if (keys[i] === 'Girls') {
                    for (var t = 0; t < orig.Girls.length; t++)
                        this.Girls[t] = new Girl(orig.Girls[t]);
                } else if (keys[i] === 'baby') {
                    for (var t = 0; t < orig.baby.length; t++)
                        this.baby[t] = new Girl(orig.baby[t]);
                } else if (keys[i] == 'newgirl') {
                    if (orig.newgirl === undefined)
                        this.newgirl = undefined;
                    else
                        this.newgirl = new Girl(orig.newgirl);
                } else if (keys[i] == 'asm') {
                    var keys2 = Object.keys(this.asm);
                    for (var t = 0; t < keys2.length; t++)
                        if (this.asm.hasOwnProperty(keys2[t]) && keys2[t] in orig.asm)
                            this.asm[keys2[t]] = deepclone(orig.asm[keys2[t]]);
                } else if (keys[i] == 'inventory') {
                    var keys2 = Object.keys(this.inventory);
                    for (var t = 0; t < keys2.length; t++)
                        if (this.inventory.hasOwnProperty(keys2[t]) && keys2[t] in orig.inventory)
                            this.inventory[keys2[t]] = deepclone(orig.inventory[keys2[t]]);
                } else if (keys[i] == 'player') {
                    this.player = new Girl(orig.player);
                } else if (isArray(this[keys[i]])) {
                    this[keys[i]] = deepclone(orig[keys[i]]);
                } else if (typeof this[keys[i]] === 'object' && !Object.isExtensible(this[keys[i]])) {
                    var keys2 = Object.keys(this[keys[i]]);
                    for (var t = 0; t < keys2.length; t++)
                        if (this[keys[i]].hasOwnProperty(keys2[t]) && keys2[t] in orig[keys[i]])
                            this[keys[i]][keys2[t]] = deepclone(orig[keys[i]][keys2[t]]);
                } else
                    this[keys[i]] = deepclone(orig[keys[i]]);
            }
            if (this.mission.farm == undefined)
                this.mission.farm = 0;
            this.sanitize_roles();
        }
    }

    Game_.prototype.sanitize_roles = function() {
        var manager = -1;
        var jailer = -1;
        var labassistant = -1;
        var headgirl = -1;
        var cook = -1;
        var nurse = -1;

        for(var i = 0; i < this.Girls.length; i++) {
            var girl = this.Girls[i];

            if (girl.hasJob('Farm Manager')) {
                if (manager != -1) {
                    ToText("You have two farm managers assigned, fixing it

");
                    girl.setJob("Rest");
                } else
                    manager = i;
            }

            if (girl.hasJob('Jailer')) {
                if (jailer != -1) {
                    ToText("You have two jailers assigned, fixing it

");
                    girl.setJob("Rest");
                } else
                    jailer = i;
            }

            if (girl.hasJob('Lab Assistant')) {
                if (labassistant != -1) {
                    ToText("You have two labassistants assigned, fixing it

");
                    girl.setJob("Rest");
                } else
                    labassistant = i;
            }

            if (girl.hasJob('Head Girl')) {
                if (headgirl != -1) {
                    ToText("You have two headgirls assigned, fixing it

");
                    girl.setJob("Rest");
                } else
                    headgirl = i;
            }

            if (girl.hasJob('Chef')) {
                if (cook != -1) {
                    ToText("You have two cooks assigned, fixing it

");
                    girl.setJob("Rest");
                } else
                    cook = i;
            }

            if (girl.hasJob('Nurse')) {
                if (nurse != -1) {
                    ToText("You have two nurses assigned, fixing it

");
                    girl.setJob("Rest");
                } else
                    nurse = i;
            }
        }

        if (this.roles.manager != manager) {
            if (manager != -1 && this.roles.manager != -1)
               ToText(this.Girls[manager] + " replaced "
                    + this.Girls[this.roles.manager].name + " as Farm Manager

");
            this.roles.manager = manager;
        }

        if (this.roles.jailer != jailer) {
            if (jailer != -1 && this.roles.jailer != -1)
               ToText(this.Girls[jailer] + " replaced "
                    + this.Girls[this.roles.jailer].name + " as Jailer

");
            this.roles.jailer = jailer;
        }

        if (this.roles.labassistant != labassistant) {
            if (labassistant != -1 && this.roles.labassistant != -1)
               ToText(this.Girls[labassistant] + " replaced "
                    + this.Girls[this.roles.labassistant].name + " as Lab Assistant

");
            this.roles.labassistant = labassistant;
        }

        if (this.roles.headgirl != headgirl) {
            if (headgirl != -1 && this.roles.headgirl != -1)
               ToText(this.Girls[headgirl] + " replaced "
                    + this.Girls[this.roles.headgirl].name + " as Headgirl

");
            this.roles.headgirl = headgirl;
        }

        if (this.roles.cook != cook) {
            if (cook != -1 && this.roles.cook != -1)
               ToText(this.Girls[cook] + " replaced "
                    + this.Girls[this.roles.cook].name + " as Cook

");
            this.roles.cook = cook;
        }

        if (this.roles.nurse != nurse) {
            if (nurse != -1 && this.roles.nurse != -1)
               ToText(this.Girls[nurse] + " replaced "
                    + this.Girls[this.roles.nurse].name + " as Nurse

");
            this.roles.nurse = nurse;
        }
    }

    Game_.prototype.Tutorial = function(id,text) {
        if (Game.settings.tutorial == 0)
            return;

        if (Game.hide_tutorials[id] == 0)
            ToText("Hide help

" + text + "

");
        else
            ToText("Show help

");
    }

    Game_.prototype.BuySpell = function(id, cost) {
            if (cost === undefined)
                    cost = 0;
            if (this.gold >= cost) {
                    this.spells_known[id] = 1;
                    this.gold -= cost;
                    PrintLocation(getAsmSys_titleCur());
            } else {
                    ToText("Learning " + Data.Spell[id].name + " costs " + "" + cost + " gold, you only have " + this.gold + ".

");
                    ToText("Return");
            }
    }

    Game_.prototype.LearnSpell = function(id) {
        if (Data.Spell[id] == undefined)
            throw new Error("Spell " + id + " is not in the system");
        this.spells_known[id] = 1;
    }

    Game_.prototype.SpellKnown = function(id) {
        if (Object.keys(Data.Spell).indexOf(id) == -1) {
            ToText("ERROR: requested spell " + id + " which is unknown to me

");
            return false;
        }
        var sp = Object.keys(this.spells_known).indexOf(id);
        if (sp == -1 || this.spells_known[sp] == 0)
            return false;
        return true;
    }

    Game_.prototype.check_new_props = function() {
        var orig = new Game_();

        var keys = Object.keys(this);
        for (var i = 0; i < keys.length; i++) {
            if (this.hasOwnProperty(keys[i]) && !(keys[i] in orig))
                console.log("New key " + keys[i]);
        }
        for (var i = 0; i < this.Girls.length; i++)
            this.Girls[i].check_new_props();

        if (this.newgirl !== undefined)
            this.newgirl.check_new_props();
    }

    Game_.prototype.trigger = function(name, args) {
        console.log("trigger: " + name);
        if (Data.trigger[name] === undefined) {
            console.log("No handler");
            return false;
        }
        if (!isArray(Data.trigger[name])) {
            console.log("Listener for " + name + " isn't an array!");
            return false;
        }

        for (var i = 0; i < Data.trigger[name].length; i++)
            if (Data.trigger[name][i](args) != false) // null return is ok
                continue;
            else
                return false;
        return true;
    }

    var Game;


code:
"use strict";

/* really simple testing framework */

var Test = {};

Test.runTestTree = function(test, name, callback, level) {
    if (level === undefined)
        level = 0;

    if (typeof(test) == "function") {
        try {
            var res = test(name, callback, level);
            if (isArray(res))
                return res;
            else if (res == true)
                return [true, callback(name, true, "OK", true, level)];
            else
                return [false, callback(name, false, "Failed", true, level)];
        } catch (e) {
            return [false, callback(name, false, e, true, level)];
        }
    }

    var res = true;
    var out = callback(name, undefined, "In progress", false, level);
    forEach(test, function(k, v) {
        var x = Test.runTestTree(v, name == "" ? k : name + "." + k, callback, level + 1);
        res = res && x[0];
        out += x[1];
    });
    out += callback(name, res, (res ? "All OK" : "Some failed"), false, level);
    return [res, out];
}

Test.tests = {};
Test.tests.tools = {};

Test.runTestsToConsole = function (tests) {
    if (tests === undefined)
        tests = Test.tests;

    var total = 0;
    var failed = 0;

    var acc = function (name, res, msg, isLeaf, level) {
        if (!isLeaf)
            return "";

        total += 1;
        if (!res) {
            failed += 1;
            console.log(name + ": ");
            console.log(msg);
        }
        return "";
    }

    console.log("Running tests");
    Test.runTestTree(tests, "", acc);
    console.log("Failed tests: " + failed + "/" + total);
    return { total: total, failed: failed };
}


code:
    (function() {
        'use strict';

        function FastClick(layer, options) {
            var oldOnClick;
            options = options || {}
            this.trackingClick = false;
            this.trackingClickStart = 0;
            this.targetElement = null;
            this.touchStartX = 0;
            this.touchStartY = 0;
            this.lastTouchIdentifier = 0;
            this.touchBoundary = options.touchBoundary || 10;
            this.layer = layer;
            this.tapDelay = options.tapDelay || 200;
            if (FastClick.notNeeded(layer)) {
                return;
            }

            function bind(method, context) {
                return function() {
                    return method.apply(context, arguments);
                }
            }
            var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
            var context = this;
            for (var i = 0, l = methods.length; i < l; i++) {
                context[methods[i]] = bind(context[methods[i]], context);
            }
            if (deviceIsAndroid) {
                layer.addEventListener('mouseover', this.onMouse, true);
                layer.addEventListener('mousedown', this.onMouse, true);
                layer.addEventListener('mouseup', this.onMouse, true);
            }
            layer.addEventListener('click', this.onClick, true);
            layer.addEventListener('touchstart', this.onTouchStart, false);
            layer.addEventListener('touchmove', this.onTouchMove, false);
            layer.addEventListener('touchend', this.onTouchEnd, false);
            layer.addEventListener('touchcancel', this.onTouchCancel, false);
            if (!Event.prototype.stopImmediatePropagation) {
                layer.removeEventListener = function(type, callback, capture) {
                    var rmv = Node.prototype.removeEventListener;
                    if (type === 'click') {
                        rmv.call(layer, type, callback.hijacked || callback, capture);
                    } else {
                        rmv.call(layer, type, callback, capture);
                    }
                }
                layer.addEventListener = function(type, callback, capture) {
                    var adv = Node.prototype.addEventListener;
                    if (type === 'click') {
                        adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
                            if (!event.propagationStopped) {
                                callback(event);
                            }
                        }), capture);
                    } else {
                        adv.call(layer, type, callback, capture);
                    }
                }
            }
            if (typeof layer.onclick === 'function') {
                oldOnClick = layer.onclick;
                layer.addEventListener('click', function(event) {
                    oldOnClick(event);
                }, false);
                layer.onclick = null;
            }
        }
        var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
        var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
        var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
        var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent);
        var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0;
        FastClick.prototype.needsClick = function(target) {
            switch (target.nodeName.toLowerCase()) {
                case 'button':
                case 'select':
                case 'textarea':
                    if (target.disabled) {
                        return true;
                    }
                    break;
                case 'input':
                    if ((deviceIsIOS && target.type === 'file') || target.disabled) {
                        return true;
                    }
                    break;
                case 'label':
                case 'video':
                    return true;
            }
            return (/\bneedsclick\b/).test(target.className);
        }
        FastClick.prototype.needsFocus = function(target) {
            switch (target.nodeName.toLowerCase()) {
                case 'textarea':
                    return true;
                case 'select':
                    return !deviceIsAndroid;
                case 'input':
                    switch (target.type) {
                        case 'button':
                        case 'checkbox':
                        case 'file':
                        case 'image':
                        case 'radio':
                        case 'submit':
                            return false;
                    }
                    return !target.disabled && !target.readOnly;
                default:
                    return (/\bneedsfocus\b/).test(target.className);
            }
        }
        FastClick.prototype.sendClick = function(targetElement, event) {
            var clickEvent, touch;
            if (document.activeElement && document.activeElement !== targetElement) {
                document.activeElement.blur();
            }
            touch = event.changedTouches[0];
            clickEvent = document.createEvent('MouseEvents');
            clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
            clickEvent.forwardedTouchEvent = true;
            targetElement.dispatchEvent(clickEvent);
        }
        FastClick.prototype.determineEventType = function(targetElement) {
            if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
                return 'mousedown';
            }
            return 'click';
        }
        FastClick.prototype.focus = function(targetElement) {
            var length;
            if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time') {
                length = targetElement.value.length;
                targetElement.setSelectionRange(length, length);
            } else {
                targetElement.focus();
            }
        }
        FastClick.prototype.updateScrollParent = function(targetElement) {
            var scrollParent, parentElement;
            scrollParent = targetElement.fastClickScrollParent;
            if (!scrollParent || !scrollParent.contains(targetElement)) {
                parentElement = targetElement;
                do {
                    if (parentElement.scrollHeight > parentElement.offsetHeight) {
                        scrollParent = parentElement;
                        targetElement.fastClickScrollParent = parentElement;
                        break;
                    }
                    parentElement = parentElement.parentElement;
                } while (parentElement);
            }
            if (scrollParent) {
                scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
            }
        }
        FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
            if (eventTarget.nodeType === Node.TEXT_NODE) {
                return eventTarget.parentNode;
            }
            return eventTarget;
        }
        FastClick.prototype.onTouchStart = function(event) {
            var targetElement, touch, selection;
            if (event.targetTouches.length > 1) {
                return true;
            }
            targetElement = this.getTargetElementFromEventTarget(event.target);
            touch = event.targetTouches[0];
            if (deviceIsIOS) {
                selection = window.getSelection();
                if (selection.rangeCount && !selection.isCollapsed) {
                    return true;
                }
                if (!deviceIsIOS4) {
                    if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {
                        event.preventDefault();
                        return false;
                    }
                    this.lastTouchIdentifier = touch.identifier;
                    this.updateScrollParent(targetElement);
                }
            }
            this.trackingClick = true;
            this.trackingClickStart = event.timeStamp;
            this.targetElement = targetElement;
            this.touchStartX = touch.pageX;
            this.touchStartY = touch.pageY;
            if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
                event.preventDefault();
            }
            return true;
        }
        FastClick.prototype.touchHasMoved = function(event) {
            var touch = event.changedTouches[0],
                boundary = this.touchBoundary;
            if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
                return true;
            }
            return false;
        }
        FastClick.prototype.onTouchMove = function(event) {
            if (!this.trackingClick) {
                return true;
            }
            if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
                this.trackingClick = false;
                this.targetElement = null;
            }
            return true;
        }
        FastClick.prototype.findControl = function(labelElement) {
            if (labelElement.control !== undefined) {
                return labelElement.control;
            }
            if (labelElement.htmlFor) {
                return $(labelElement.htmlFor);
            }
            return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
        }
        FastClick.prototype.onTouchEnd = function(event) {
            var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
            if (!this.trackingClick) {
                return true;
            }
            if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
                this.cancelNextClick = true;
                return true;
            }
            this.cancelNextClick = false;
            this.lastClickTime = event.timeStamp;
            trackingClickStart = this.trackingClickStart;
            this.trackingClick = false;
            this.trackingClickStart = 0;
            if (deviceIsIOSWithBadTarget) {
                touch = event.changedTouches[0];
                targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
                targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
            }
            targetTagName = targetElement.tagName.toLowerCase();
            if (targetTagName === 'label') {
                forElement = this.findControl(targetElement);
                if (forElement) {
                    this.focus(targetElement);
                    if (deviceIsAndroid) {
                        return false;
                    }
                    targetElement = forElement;
                }
            } else if (this.needsFocus(targetElement)) {
                if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
                    this.targetElement = null;
                    return false;
                }
                this.focus(targetElement);
                this.sendClick(targetElement, event);
                if (!deviceIsIOS || targetTagName !== 'select') {
                    this.targetElement = null;
                    event.preventDefault();
                }
                return false;
            }
            if (deviceIsIOS && !deviceIsIOS4) {
                scrollParent = targetElement.fastClickScrollParent;
                if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
                    return true;
                }
            }
            if (!this.needsClick(targetElement)) {
                event.preventDefault();
                this.sendClick(targetElement, event);
            }
            return false;
        }
        FastClick.prototype.onTouchCancel = function() {
            this.trackingClick = false;
            this.targetElement = null;
        }
        FastClick.prototype.onMouse = function(event) {
            if (!this.targetElement) {
                return true;
            }
            if (event.forwardedTouchEvent) {
                return true;
            }
            if (!event.cancelable) {
                return true;
            }
            if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
                if (event.stopImmediatePropagation) {
                    event.stopImmediatePropagation();
                } else {
                    event.propagationStopped = true;
                }
                event.stopPropagation();
                event.preventDefault();
                return false;
            }
            return true;
        }
        FastClick.prototype.onClick = function(event) {
            var permitted;
            if (this.trackingClick) {
                this.targetElement = null;
                this.trackingClick = false;
                return true;
            }
            if (event.target.type === 'submit' && event.detail === 0) {
                return true;
            }
            permitted = this.onMouse(event);
            if (!permitted) {
                this.targetElement = null;
            }
            return permitted;
        }
        FastClick.prototype.destroy = function() {
            var layer = this.layer;
            if (deviceIsAndroid) {
                layer.removeEventListener('mouseover', this.onMouse, true);
                layer.removeEventListener('mousedown', this.onMouse, true);
                layer.removeEventListener('mouseup', this.onMouse, true);
            }
            layer.removeEventListener('click', this.onClick, true);
            layer.removeEventListener('touchstart', this.onTouchStart, false);
            layer.removeEventListener('touchmove', this.onTouchMove, false);
            layer.removeEventListener('touchend', this.onTouchEnd, false);
            layer.removeEventListener('touchcancel', this.onTouchCancel, false);
        }
        FastClick.notNeeded = function(layer) {
            var metaViewport;
            var chromeVersion;
            var blackberryVersion;
            if (typeof window.ontouchstart === 'undefined') {
                return true;
            }
            chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [, 0])[1];
            if (chromeVersion) {
                if (deviceIsAndroid) {
                    metaViewport = document.querySelector('meta[name=viewport]');
                    if (metaViewport) {
                        if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
                            return true;
                        }
                        if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {
                            return true;
                        }
                    }
                } else {
                    return true;
                }
            }
            if (deviceIsBlackBerry10) {
                blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/);
                if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) {
                    metaViewport = document.querySelector('meta[name=viewport]');
                    if (metaViewport) {
                        if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
                            return true;
                        }
                        if (document.documentElement.scrollWidth <= window.outerWidth) {
                            return true;
                        }
                    }
                }
            }
            if (layer.style.msTouchAction === 'none') {
                return true;
            }
            return false;
        }
        FastClick.attach = function(layer, options) {
            return new FastClick(layer, options);
        }
        if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
            define(function() {
                return FastClick;
            });
        } else if (typeof module !== 'undefined' && module.exports) {
            module.exports = FastClick.attach;
            module.exports.FastClick = FastClick;
        } else {
            window.FastClick = FastClick;
        }
    }());


code:
"use strict";

// Whenever game still uses numeric id's
var NUMERIC_IDS = true;

Test.tests.tools.types = {};
var t = Test.tests.tools.types;

/*  Some magic to implement types in JS

    Each type is

    {
        get: function() {...}          // getter, "this" is set to current stored internal value
                                       //         returns the external representation of the internal value
        set: function(a) {...}         // setter, "this" is set to current stored value
                                       //         updates "this" with "a" and returns new "this"

        // optional
        construct: function(...) {...} // constructor, like set, but with arbitrary arguments
        ... whatever else ...          // this will be available in type reflection
    }

    TODO explanation how it works.

*/

/* Working with typed values */

// Assign: update x with y
var assign = function(type, x, y) {
    return type.set.call(x, y);
}

// Cast value to type
var cast = function(type, value) {
    return type.set.call(undefined, value);
}

// ...
var uncast = function(type, value) {
    return type.get.call(value);
}

// ...
var typeAs = function(type, value) {
    return uncast(type, cast(type, value));
}

// Make typed value into a getter
var GetterOf = function(type, fun) {
    return function() {
        var res = fun.call(this);
        return typeAs(type, res);
    };
}

/* Simple types */

// Anything
var Any = {
    get: function() { return this; },
    set: function(a) { return a; },
}

// Boolean
var Bool = {
    get: function() { return this; },
    set: function(a) {
        if (a instanceof Boolean)
            a = a.valueOf();

        if (NUMERIC_IDS) {
            if (a === 0) a = false;
            else if (a === 1) a = true;
        }

        if (typeof a !== "boolean")
            throw Fail("Trying to cast", a, "to Bool");

        return a;
    },
    from: function(a) { return new Boolean(Bool.set(a)); },
}

t.Bool = function() {
    var x = cast(Bool, false);
    x = uncast(Bool, x);
    if (x != false || x !== false || x != 0)
        throw Fail("Something is not right here", x);

    var x = cast(Bool, true);
    x = uncast(Bool, x);
    if (x != true || x !== true || x != 1)
        throw Fail("Something is not right here", x);

    var error = true;

    try {
        cast(Bool, []);
        error = false;
    } catch(e) {}

    try {
        cast(Bool, "test");
        error = false;
    } catch(e) {}

    if (!error)
        throw Fail("Something is not right here");

    error = true;

    try {
        cast(Bool, 0);
        error = false;
    } catch(e) {}

    if (!NUMERIC_IDS && !error)
        throw Fail("Something is not right here");

    return true;
}

// Number
var Num = {
    get: function() { return this; },
    set: function(a) {
        if (a instanceof Number)
            a = a.valueOf();

        if (typeof a !== "number")
            throw Fail("Trying to cast", a, "to Num");

        if (Number.isNaN(a))
            throw Fail("Trying to cast NaN");

        return a;
    },
    from: function(a) { return new Number(Num.set(a)); },
}

// Integer
var Int = {
    get: Num.get,
    set: function(a) {
        a = Num.set(a);

        if (!Number.isInteger(a))
            throw Fail("Trying to cast", a, "to Int");

        return a;
    },
    from: function(a) { return new Number(Int.set(a)); },
    random: function() { return Math.tossDice(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); },
}

// Natural
var Nat = {
    get: Int.get,
    set: function(a) {
        a = Int.set(a);

        if (a < 0)
            throw Fail("Trying to cast", a, "to Nat");

        return a;
    },
    from: function(a) { return new Number(Nat.set(a)); },
    random: function() { return Math.tossDice(0, Number.MAX_SAFE_INTEGER); },
}

t.Numbers = function() {
    var x = cast(Num, 0);
    x = uncast(Num, x);
    if (x != 0)
        throw Fail("Something is not right here", x);

    var x = cast(Num, -1);
    x = uncast(Num, x);
    if (x != -1)
        throw Fail("Something is not right here", x);

    var x = cast(Num, 0.5);
    x = uncast(Num, x);
    if (x != 0.5)
        throw Fail("Something is not right here", x);

    var x = cast(Num, Num.from(0.5));
    x = uncast(Num, x);
    if (x != 0.5)
        throw Fail("Something is not right here", x);

    var error = true;

    try {
        cast(Num, []);
        error = false;
    } catch(e) {}

    try {
        cast(Num, NaN);
        error = false;
    } catch(e) {}

    try {
        cast(Num, "foo");
        error = false;
    } catch(e) {}

    if (!error)
        throw Fail("Something is not right here");

    var error = true;

    var x = cast(Int, 0);
    x = uncast(Int, x);
    if (x != 0)
        throw Fail("Something is not right here", x);

    try {
        cast(Int, 0.5);
        error = false;
    } catch(e) {}

    if (!error)
        throw Fail("Something is not right here");

    var x = cast(Nat, 0);
    x = uncast(Nat, x);
    if (x != 0)
        throw Fail("Something is not right here", x);

    var error = true;

    try {
        cast(Nat, -1);
        error = false;
    } catch(e) {}

    if (!error)
        throw Fail("Something is not right here");

    return true;
}

// String
var Str = {
    get: function() { return this; },
    set: function(a) {
        if (a instanceof String)
            a = a.valueOf();

        if (typeof a !== "string")
            throw Fail("Trying to cast", a, "to Str");

        return a;
    },
    from: function(a) { return new String(Str.set(a)); },
}

t.Str = function() {
    var x = cast(Str, "test");
    x = uncast(Str, x);
    if (x != "test")
        throw Fail("Something is not right here", x);

    var x = cast(Str, Str.from("test"));
    x = uncast(Str, x);
    if (x != "test")
        throw Fail("Something is not right here", x);

    var error = true;

    try {
        cast(Str, 0);
        error = false;
    } catch(e) {}

    if (!error)
        throw Fail("Something is not right here");

    return true;
}

/* FieldProxy: a Proxy that disallows references to nonexistent fields
   and maps all values with given getter and setter.

   Doesn't work with .valueOf().
*/

var FieldProxy = function(target, setNew, getter, setter, primitive) {
    if (primitive === undefined)
        primitive = function() {
            if (Symbol.toPrimitive in this)
                return this[Symbol.toPrimitive].apply(this, arguments);
            else
                return this.valueOf();
        };

    var _has = function(target, field) {
        if (field in target || field === "__target")
            return true;
        else
            return false;
    }

    var _getter = function(target, field) {
        if (field in target) {
            if (getter === undefined)
                return target[field];
            else
                return getter.call(target, field);
        } else if (field === Symbol.toPrimitive) {
            return function() {
                //this is a Proxy, override with target
                return primitive.call(target)
            };
        } else if (field === "__target") {
            return target;
        } else
            throw Fail("Trying to get nonexistent field", field, "in", target);
    }

    var _setter = function(target, field, value) {
        if (field in target || setNew) {
            if (setter === undefined)
                target[field] = value;
            else
                setter.call(target, field, value);
        } else
            throw Fail("Trying to set nonexistent field", field, "in", target, "to", value);

        return true;
    }

    return new Proxy(target, { has: _has, get: _getter, set: _setter });
}

/* DefinedFieldsOnly: trivial FieldProxy */

var DFO = function(target, primitive) {
    return FieldProxy(target, false, undefined, undefined, primitive);
}

var unDFO = function(x) {
    if (typeof x === "object" && "__target" in x)
        x = x.__target;
    return x;
}

// DFO with read-only props
var DFOWithRO = function(target, enumerable, props) {
    Object.defineReadOnlyProperties(target, enumerable, props);
    return DFO(target);
}

t.DFO = function() {
    var x = DFO({
        a: 0,
        b: "foo",
    });

    // check it works like an object
    x.a = 1;
    x.b = "bar";

    if (x.a != 1 || x.b != "bar")
        throw Fail("Something is not right here");

    var fields = [];
    forEach(x, function(k, v) {
        fields.push(k);
    });

    if (fields.length != 2 || fields[0] != "a" || fields[1] != "b")
        throw Fail("Something is not right here");

    // check its a DFOness
    var error = true;

    try {
        var y = x.c;
        error = false;
    } catch (e) {}

    try {
        x.c = "baz";
        error = false;
    } catch (e) {}

    if (!error)
        throw Fail("Something is not right here");

    return true;
}

/* Type generators */

// Clamped number with descriptions
var ClampedTiers = function(min, tiers, max, _descs, isfloat) {
    if (isfloat === undefined)
        isfloat = "round";

    if ([true, false, "round", "floor", "ceil"].indexOf(isfloat) === -1)
        throw Fail("Unexpected float type", isfloat);

    if (_descs === undefined)
        _descs = {};

    var findIndex = function(self) {
        var index = 0;
        for (var i=0; i<tiers.length; i++) {
            if (self >= tiers[i])
                index = i+1;
            else
                break;
        }
        return index;
    }

    var descs = {};
    forEach(_descs, function(k, v) {
        if (isArray(v)) {
            if (v.length != tiers.length + 1)
                throw new Error("Tiers and their descriptions don't match");

            descs[k] = function(self, index) {
                if (index === undefined)
                    index = findIndex(self);
                return v[index];
            };
        } else {
            descs[k] = v;
        }
    });

    var setter = function(a) {
        a = unDFO(a);
        if (isfloat === false)
            a = Int.set(a);
        else
            a = Num.set(a);

        if (isfloat === "round")
            a = Math.round(a);
        else if (isfloat === "floor")
            a = Math.floor(a);
        else if (isfloat === "ceil")
            a = Math.ceil(a);

        return Math.clamp(min, a, max);
    };

    var getter = function() {
        var self = this;
        var index = findIndex(self);
        var x = Num.from(self);
        var p = {};
        p.value = self;
        p.index = index;
        forEach(descs, function(k, v) {
            p[k] = v.call(p, self, index);
        });
        return DFOWithRO(x, false, p);
    };

    var props = {
        get: getter,
        set: setter,
        min: min,
        max: max,
        tiers: tiers,
        derived: descs,
        random: (isfloat === true) ? function() {
            return getter.call(Math.tossValue(min, max));
        } : function() {
            return getter.call(Math.tossDice(min, max));
        },
    };

    // create reflection object
    return DFOWithRO({}, true, props);
}

t.ClampedTiers = function(name, callback, level) {
    var test = function(float) {
        var type = ClampedTiers(-100, [50], 100, {
            tiers: [ "less", "more" ],
            func1: function(self) {
                return self + 10;
            },
            func2: function() {
                return this.tiers;
            },
        }, float);

        var x = cast(type, -101);
        x = uncast(type, x);
        if (x != -100 || x.value != -100 || x.index != 0 || x.tiers != "less" || x.func1 != -90 || x.func2 != x.tiers)
            throw Fail("Something is not right here", x);

        x = cast(type, 101);
        x = uncast(type, x);
        if (x != 100 || x.value != 100 || x.index != 1 || x.tiers != "more" || x.func1 != 110 || x.func2 != x.tiers)
            throw Fail("Something is not right here", x);

        x = cast(type, 50.5);
        x = uncast(type, x);
        switch(float) {
        case true:
            if (x != 50.5) throw Fail("Something is not right here", x);
            break;
        case "floor":
            if (x != 50) throw Fail("Something is not right here", x);
            break;
        case "round":
            if (x != 51) throw Fail("Something is not right here", x);
            break;
        case "ceil":
            if (x != 51) throw Fail("Something is not right here", x);
            break;
        }

        var error = true;

        try {
            x = cast(type, "foo");
            error = false;
        } catch(e) {}

        try {
            x = cast(type, NaN);
            error = false;
        } catch(e) {}

        if (!error)
            throw Fail("Something is not right here");

        return true;
    }

    return Test.runTestTree({
        floor: function() { return test("floor"); },
        round: function() { return test("round"); },
        ceil: function() { return test("ceil"); },
        float: function() { return test(true); },
    }, name, callback, level);
}

// Tagged union with descriptions
var Union = function(tagtype, values, sndtypes, _descs, numeric) {
    if (_descs === undefined)
        _descs = {};
    if (numeric === undefined)
        numeric = NUMERIC_IDS;

    var descs = {};
    forEach(_descs, function(k, v) {
        if (isArray(v)) {
            if (v.length != values.length)
                throw new Error("Values and their descriptions don't match");
            descs[k] = function(n, i) { return v[i]; };
        } else {
            descs[k] = v;
        }
    });

    var toJSON = function () {
        var res = unDFO(this.value);
        if (numeric) res = this.index;

        var type = sndtypes[this.value];
        if (type !== undefined) {
            if (this.snd.toJSON !== undefined)
                return Tagged(res, unDFO(this.snd).toJSON());
            else
                return Tagged(res, unDFO(this.snd).valueOf());
        }
        else
            return res;
    };

    var valueOf = function () {
        var res = unDFO(this.value);
        if (numeric) res = this.index;

        var type = sndtypes[this.value];
        if (type !== undefined)
            return Tagged(res, unDFO(this.snd).valueOf());
        else
            return res;
    };

    var _setter = function(tag, index, snd) {
        var res = tagtype.from(tag);

        var p = {
            value: tag,
            tag: tag,
            index: index,
            toJSON: toJSON,
            valueOf: valueOf,
        };
        forEach(descs, function(k, v) {
            p[k] = v.call(p, tag, index);
        });
        Object.defineReadOnlyProperties(res, false, p);

        var type = sndtypes[tag];
        if (type === undefined) {
            if (snd !== undefined)
                throw Fail("There must be no value associated to", tag);
            else
                return res;
        }

        // reuse current snd when tag stays the same
        if (this !== undefined && this.index === index)
            res.__snd = this.__snd;

        Object.defineProperty(res, "snd", {
            enumerable: true,
            get: function() {
                return uncast(type, this.__snd);
            },
            set: function(a) {
                this.__snd = assign(type, this.__snd, a);
            },
        });

        if (snd === undefined) {
            if ("construct" in type)
                snd = new type.construct();
            else
                throw Fail("No default constructor for value associated to", tag);
        }

        res.snd = snd;
        return res;
    };

    var setter = function(a) {
        a = unDFO(a);

        var _a = a;
        if (typeof a === "object" && "tag" in a) // for Tagged
            a = a.tag;

        if (Number.isInteger(a)) {
            if (!numeric)
                throw Fail("Using numeric id", a, "expecting", values);

            a = Int.set(a);

            if (a < 0 || a > values.length - 1)
                throw Fail("Out of range:", a, "expecting", values);

            return _setter(values[a], a, _a.snd);
        }

        var index = values.indexOf(a);
        if (index === -1)
            throw Fail("Out of range:", "(" + typeof(a) + ")", a, "expecting", values);

        return _setter(a, index, _a.snd);
    };

    var getter = function() {
        return DFO(this, function(){
            return unDFO(this.tag).valueOf();
        });
    };

    var props = {
        get: getter,
        set: setter,
        values: values,
        s: sndtypes,
        derived: descs,
        random: function() {
            var index = Math.tossIndex(values.length);
            var value = values[index];
            var type = sndtypes[value];
            if (type !== undefined) {
                return getter.call(setter(Tagged(value, type.random())));
            } else
                return getter.call(setter(value));
        },
    };

    // create reflection object
    return DFOWithRO({}, true, props);
}

var Tagged = function(tag, snd) {
    return {
        tag: tag,
        snd: snd,
    };
}

t.Union = function() {
    var type = Union(Str, ["nope", "yes", "nat"], {
        nat: Nat,
    }, {
        tiers: [ true, false, "whatever" ],
        func1: function(self, index) {
            return "" + self + index;
        },
        func2: function() {
            return this.tiers;
        },
    }, false);

    var x = cast(type, "nope");
    x = uncast(type, x);
    if (x != "nope" || x.value != "nope" || x.index != 0 || x.tiers != true || x.func1 != "nope0" || x.func2 != x.tiers || "snd" in x)
        throw Fail("Something is not right here", x);

    var x = cast(type, "yes");
    x = uncast(type, x);
    if (x != "yes" || x.value != "yes" || x.index != 1 || x.tiers != false || x.func1 != "yes1" || x.func2 != x.tiers || "snd" in x)
        throw Fail("Something is not right here", x);

    var x = cast(type, Tagged("nat", 0));
    x = uncast(type, x);
    if (x != "nat" || x.value != "nat" || x.index != 2 || x.tiers != "whatever" || x.func1 != "nat2" || x.func2 != x.tiers || x.snd != 0)
        throw Fail("Something is not right here", x);

    for (var i = 0; i < 100; i++) {
        var x = type.random();
    }

    var error = true;

    try {
        x = cast(type, Tagged("yes", 0));
        error = false;
    } catch(e) {}

    try {
        x = cast(type, Tagged("nat", -1));
        error = false;
    } catch(e) {}

    try {
        x = cast(type, "foo");
        error = false;
    } catch(e) {}

    try {
        x = cast(type, NaN);
        error = false;
    } catch(e) {}

    try {
        x = cast(type, 2);
        error = false;
    } catch(e) {}

    if (!error)
        throw Fail("Something is not right here");

    return true;
}

var Enum = function(values, _descs, numeric) {
    return Union(Str, values, {}, _descs, numeric);
}

// Boolean with descriptions
var Flag = function(_descs) {
    return Union(Bool, [false, true], {}, _descs);
};

t.Flag = function() {
    var type = Flag({
        tiers: [ "nope", "yes" ],
        func1: function(self) {
            return !self;
        },
        func2: function() {
            return this.tiers;
        },
    });

    var test = function(a, e, t, f) {
        var x = cast(type, a);
        x = uncast(type, x);
        if (x != e || x.value != e || x.tiers != t || x.func1 != f || x.func2 != x.tiers)
            throw Fail("Something is not right here", x);
    }

    test(false, false, "nope", true);
    test(false, 0, "nope", true);
    if (NUMERIC_IDS)
        test(0, false, "nope", true);

    test(true, true, "yes", false);
    test(true, 1, "yes", false);
    if (NUMERIC_IDS)
        test(1, true, "yes", false);

    /* This doesn't work in JS, because, pause

         if(new Boolean(false)) return true; => true

       PERFECT!

    if (x)
        throw Fail("Something is not right here", x);

    if (!x)
        throw Fail("Something is not right here", x);
    */

    var error = true;

    try {
        x = cast(type, "foo");
        error = false;
    } catch(e) {}

    try {
        x = cast(type, NaN);
        error = false;
    } catch(e) {}

    try {
        x = cast(type, 2);
        error = false;
    } catch(e) {}

    if (!error)
        throw Fail("Something is not right here");

    return true;
}

// Array of typed things
var ArrayOf = function(element, descs) {
    if (descs === undefined)
        descs = {};

    // constructor
    var constructor = function(arg) {
        Array.constructor.apply(this);
        if (arg === undefined)
            return;

        for (var i = 0; i < arg.length; i++) {
            this.push(assign(element, undefined, arg[i]));
        }
    }

    constructor.prototype = Object.create(Array.prototype);
    constructor.prototype.constructor = constructor;
    constructor.prototype.valueOf = function() {
        var res = new Array();
        for (var i = 0; i < this.length; i++) {
            res.push(unDFO(this[i]).valueOf());
        }
        return res;
    };
    constructor.prototype.toJSON = function() {
        var res = new Array();
        for (var i = 0; i < this.length; i++) {
            var t = unDFO(this[i]);
            if (t.toJSON !== undefined)
                res.push(t.toJSON());
            else
                res.push(t.valueOf());
        }
        return res;
    };

    // internal get/set
    var elSetter = function(field, a) {
        if (field in constructor.prototype)
            this[field] = a;
        else
            this[field] = assign(element, this[field], a);
    };

    var elGetter = function(field) {
        if (field in constructor.prototype)
            return this[field];
        else
            return uncast(element, this[field]);
    };

    // derived properties
    var p = {};
    forEach(descs, function(k, v) {
        p[k] = function() {
            return v.call(FieldProxy(this, false, elGetter, elSetter));
        };
    });
    Object.defineGetterProperties(constructor.prototype, false, p);

    // external get/set
    var setter = function(a) {
        var res = FieldProxy(new constructor(), true, elGetter, elSetter);
        for (var i = 0; i < a.length; i++) {
            res.push(a[i]);
        }
        return res; // use FieldProxy as internal representation
    };

    var getter = function() {
        return this;
    };

    var props = {
        construct: constructor,
        get: getter,
        set: setter,
        e: element,
        derived: descs,
    };

    // create reflection object
    var reflection = DFOWithRO({}, true, props);
    constructor.prototype.__type = reflection;
    return reflection;
}

t.ArrayOf = function() {
    var type = ArrayOf(Enum(["nope", "yes"]), {
        func1: function() { return this; }
    });

    var x = cast(type, []);
    x = uncast(type, x);
    if (x.length != 0)
        throw Fail("Something is not right here", x.__target);

    x.push("nope");
    if (x.length != 1 || x[0].value != "nope" || x.func1[0] != "nope" || x.func1.func1[0] != "nope")
        throw Fail("Something is not right here", x.__target);

    var x = cast(type, ["yes"]);
    x = uncast(type, x);
    if (x.length != 1 || x[0].value != "yes" || x.func1[0] != "yes" || x.func1.func1[0] != "yes")
        throw Fail("Something is not right here", x.__target);

    x.push("nope");
    if (x.length != 2 || x[1].value != "nope" || x.func1[1] != "nope" || x.func1.func1[1] != "nope")
        throw Fail("Something is not right here", x.__target);

    var x = cast(type, []);
    var y = cast(type, []);
    x = assign(type, x, y);
    if (x.length != 0)
        throw Fail("Something is not right here", x.__target);

    var x = cast(type, ["nope"]);
    var y = cast(type, []);
    x = assign(type, x, y);
    if (x.length != 0)
        throw Fail("Something is not right here", x.__target);

    var x = cast(type, []);
    var y = cast(type, ["yes"]);
    x = assign(type, x, y);
    if (x.length != 1 || x[0].value != "yes")
        throw Fail("Something is not right here", x.__target);

    return true;
}

// Typed Object, this is where all the magic happens
var Properties = function(props, theconstructor, theassigner) {
    var pproto = {};        // stuff added to prototype
    var ptypes = {};        // children types to be added to prototype
    var pconstructors = {}; // their constructors
    var pinits = {};        // and call arguments for their constructors
    var pdefaults = {};     // simple default assignments
    var pchildren = {};     // type reflection

    forEach(props, function(k, v) {
        if (k.match(/^proto\./)) { // simply copy to prototype
            k = k.substr(6);
            if (k in pproto)
                throw Fail("Redefining " + k);

            pproto[k] = {
                enumerable: true,
                value: v,
            };
        } else if (k.match(/^get\./)) { // simple getter
            k = k.substr(4);
            if (k in pproto) {
                if ("value" in pproto[k] || "get" in pproto[k])
                    throw Fail("Redefining " + k);
            } else
                pproto[k] = { enumerable: true };

            pproto[k].get = v;
        } else if (k.match(/^set\./)) { // simple setter
            k = k.substr(4);
            if (k in pproto) {
                if ("value" in pproto[k] || "set" in pproto[k])
                    throw Fail("Redefining " + k);
            } else
                pproto[k] = { enumerable: true };

            pproto[k].set = v;
        } else if (k.match(/^type\./)) { // type definition
            k = k.substr(5);             // use its getters and setters
            if (k in ptypes)
                throw Fail("Redefining " + k);

            ptypes[k] = { enumerable: true };

            pchildren[k] = v;

            if ("construct" in v)
                pconstructors[k] = v.construct;

            if ("get" in v)
                ptypes[k].get = function() {
                    //console.log("get field", k, " this ", this.__impl[k]);
                    try {
                        return uncast(v, this.__impl[k]);
                    } catch(e) {
                        throw Fail("Getting field " + k + ":", e);
                    }
                };
            if ("set" in v)
                ptypes[k].set = function(a) {
                    //console.log("set field", k, " this ", this.__impl[k], " to ", a);
                    try {
                        this.__impl[k] = assign(v, this.__impl[k], a);
                    } catch(e) {
                        throw Fail("Setting field " + k + ":", e);
                    }
                };
        } else if (k.match(/^init\./)) { //constructor arguments
            k = k.substr(5);
            if (!(k in ptypes))
                throw Fail("No type signature for", k);

            pinits[k] = v;
        } else { // assignment of a simple value
            if (!(k in ptypes))
                throw Fail("No type signature for", k);

            pdefaults[k] = v;
        }
    });

    //sanity checks
    forEach(pproto, function(k, v) {
        if (k in ptypes)
            throw Fail("Redefining " + k);
    });

    forEach(ptypes, function(k, v) {
        if (k in pproto)
            throw Fail("Redefining " + k);

        if (!(k in pconstructors || k in pinits || k in pdefaults))
            throw Fail("Don't know how to construct " + k);

        if (k in pinits && k in pdefaults)
            throw Fail("Using both init and default value " + k);
    });

    // default constructor
    var constructor = function() {
        var self = this;
        // store for internal properties
        Object.defineProperty(this, "__impl", {
            value: {},
            writable: true,
            // not enumerable
        });

        // constructing fields
        forEach(pconstructors, function(k, v) {
            if (!(k in pdefaults || k in pinits)) // has no assignemnt -> call default constructor
                self[k] = new v();
        });
        forEach(pinits, function(k, v) { // run custom constructor
            self[k] = v();
        });
        forEach(pdefaults, function(k, v) { // just try to assign
            self[k] = v;
        });

        // calling user-specified stuff
        if (theconstructor !== undefined)
            theconstructor.apply(this, arguments);

        Object.preventExtensions(this);
    };

    // dumping to plain Object
    var valueOf = function (){
        var self = this;
        res = {};
        var dump = function(k) {
            var v = self[k];
            res[k] = unDFO(v).valueOf();
        };
        forEach(this, dump); // dump simple properties
        forEach(this.__impl, dump); // dump internal stuff
        return res;
    };

    // dumping to JSON, remove when NUMERIC_IDS becomes false
    var toJSON = function () {
        var self = this;
        res = {};
        var dump = function(k) {
            var v = unDFO(self[k]);
            if (v.toJSON !== undefined)
                res[k] = v.toJSON();
            else
                res[k] = v.valueOf();
        };
        forEach(this, dump); // dump simple properties
        forEach(this.__impl, dump); // dump internal stuff
        return res;
    };

    // dumping
    constructor.prototype.valueOf = valueOf;
    constructor.prototype.toJSON = NUMERIC_IDS ? toJSON : valueOf;

    // add getters and setters to the prototype
    Object.defineProperties(constructor.prototype, pproto);
    Object.defineProperties(constructor.prototype, ptypes);

    // this checks types on assignment
    var setter = function(a) {
        var self = this;
        if (self === undefined)
            self = new constructor();

        var visited = {};

        var assign = function(k) {
            if (visited[k] === true) throw new Error("BUG");

            if (a !== undefined && k in a) {
                self[k] = a[k];
                visited[k] = true;
            } else
                throw Fail("Missing property", k, "when assigning", self, "from", a);
        }

        forEach(self, assign); // assign simple properties
        forEach(self.__impl, assign); // assign internal stuff

        // checking there's nothing left
        forEach(a, function(k) {
            if (k in a && visited[k] != true) {
                throw Fail("Orphaned property", k, "when assigning", self, "from", a);
            }
        });

        // calling user-specified stuff
        if (theassigner !== undefined)
            theassigner.apply(self, arguments);

        return self;
    };

    var getter = function() {
        return DFO(this);
    };

    var res = {
        construct: constructor,
        get: getter,
        set: setter,
        c: DFOWithRO({}, true, pchildren), // list children types for reflection
    };

    // create reflection object
    var reflection = DFOWithRO({}, true, res)
    constructor.prototype.__type = reflection;

    return reflection;
}

t.Properties = function() {
    //constructors
    var type = Properties({
        "type.a": Num,
        a: 0,
    });

    var p = new type.construct();

    var error = true;

    try {
        var type = Properties({
            "type.a": Num,
            a: "test",
        });
        var p = new type.construct();
        error = false;
    } catch(e) {}

    if (!error)
        throw Fail("Something is not right here");

    var type = Properties({
        "type.a": Num,
        a: 0,

        "type.b": Enum(["nope", "yes"]),
        b: "nope",
    });

    var p = new type.construct();

    //setters
    var x = cast(type, { a: 1, b: "yes" });
    if (x.a != 1 || x.b != "yes")
        throw Fail("Something is not right here", x);

    assign(type, p, x);
    if (p.a != 1 || p.b != "yes")
        throw Fail("Something is not right here", p);

    //field setters
    p.a = 100;
    p.b = "nope";

    if (p.a != 100 || p.b != "nope")
        throw Fail("Something is not right here", p);

    var error = true;

    //partial assignments
    try {
        assign(type, p, { a: 0 });
        error = false;
    } catch(e) {}

    try {
        assign(type, p, { b: "nope" });
        error = false;
    } catch(e) {}

    //assignments to unexistant fields
    try {
        p.foo = "bar";
        error = false;
    } catch(e) {}

    try {
        assign(type, p, { a: 0, b: "nope", foo: "bar" });
        error = false;
    } catch(e) {}

    //wrong types
    try {
        p.a = "nope";
        error = false;
    } catch(e) {}

    try {
        p.b = 0;
        error = false;
    } catch(e) {}

    try {
        assign(type, p, { a: "nope", b: "nope" });
        error = false;
    } catch(e) {}

    try {
        assign(type, p, { a: 0, b: 0 });
        error = false;
    } catch(e) {}

    //direct field assignments assign by reference
    var type = Properties({
        "type.a": Any,
        a: { old: true },
    });

    var p = new type.construct();
    var old_a = p.a;
    var new_a = { old: false };

    p.a = new_a;

    if (p.a != new_a || p.a === old_a)
        throw Fail("Something is not right here", p);

    //constuctors assign by reference
    var p = new type.construct();
    if (p.a != old_a)
        throw Fail("Something is not right here", p);

    //indirect assignments assign by reference
    assign(type, p, { a: new_a });
    if (p.a != new_a || p.a === old_a)
        throw Fail("Something is not right here", p);

    //except when assigning Properties
    //in which case they get copied field by field
    var sup = Properties({
        "type.sub": type,
    });

    var t = new sup.construct();
    var old_sub = t.sub.__target;
    var new_sub = { a: new_a };

    t.sub = new_sub;

    if (t.sub.__target === new_sub || t.sub.__target != old_sub || t.sub.a != new_a)
        throw Fail("Something is not right here", t);

    return true;
}


code:
"use strict";

Test.tests.tools.util = {};
var t = Test.tests.tools.util;

/* exceptions */

// Error, but joins message from the arguments and also logs them on console
// Throw without "new" keyword
var Fail = function() {
    var error = new Error();
    error.name = "Fail";

    var args = [];
    forEach(arguments, function(k, v) { args.push(v); }); // looks prettier in Firefox
    console.log(args);

    error.message = " ".join(args);
    return error;
}

/* various utils */

window.forEach = function(obj, fn) {
    var keys = Object.keys(obj);
    for(var i = 0; i< keys.length;i++) {
        if (obj.hasOwnProperty(keys[i]))
            fn(keys[i], obj[keys[i]]);
    }
}

t.forEach = function() {
    var x = [0, 1, 2];
    x[-1] = -1;
    x[-2] = -2;

    forEach(x, function(k, v) {
        if (k < -2 || k > 2 || k != x[k])
            throw Fail("Something is not right here");
    });
    return true;
}

window.deepclone = function(obj, cloneloop) {
  if (cloneloop == undefined)
      cloneloop = [];
  if (obj === null || typeof(obj) !== 'object' || cloneloop.indexOf(obj) != -1)
    return obj;

  if (obj instanceof Date)
      var temp = new obj.constructor(); //or new Date(obj);
  else if (isArray(obj))
      var temp = [];
  else if ("__type" in obj) // magical types have their own assignment
      return cast(obj.__type, obj);
  else
      var temp = new Object();

  temp.__proto__ = obj.__proto__;

  for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
          cloneloop.push(obj);
          temp[key] = deepclone(obj[key], cloneloop);
          cloneloop.splice(cloneloop.indexOf(obj));
      }
  }
  if (false && isArray(obj)) {
     Object.defineProperty(temp, 'length', {
         enumerable: false,
         configurable: false,
         writable: true,
         value: obj.length});
  }

  return temp;
}

window.simple_template = function(str, data) {
    return str.replace(/{{(\w*)}}/g,function(m,key){return key in data ? data[key]:'{X}' + key + '}}';});
}

window.unixName = function (str) {
    return str.toLowerCase().replace(/[^a-z]/g, '_');
}

/* strings */

String.prototype.capitalize = function() {
    return this.charAt(0).toUpperCase() + this.slice(1);
}

String.prototype.join = function(arr) {
    var out = "";
    var first = true;
    var sep = this;
    forEach(arr, function(k, v) {
        if (!first)
            out += sep + v;
        else {
            out = v;
            first = false;
        }
    });
    return out;
}

/* objects */

Object.forEach = window.forEach; // placeholder until dicts.

// Add a bunch of read-only properties to obj
Object.defineReadOnlyProperties = function(obj, enumerable, props) {
    forEach(props, function(k, v) {
        Object.defineProperty(obj, k, {
            value: v,
            enumerable: enumerable,
        });
    });
}

t.defineReadOnlyProperties = function() {
    var x = {};
    Object.defineReadOnlyProperties(x, true, { foreverFoo: "foo" });
    var foo = x.foreverFoo;
    if (foo != "foo")
        throw new Error("Something is not right here");

    try {
        x.foreverFoo = "bar";
    } catch (e) {
        if (e instanceof TypeError)
            return true;
    }
    throw new Error("makeReadOnly result is not read-only");
}

// defineReadOnlyProperties but for getters
Object.defineGetterProperties = function(obj, enumerable, props) {
    forEach(props, function(k, v) {
        Object.defineProperty(obj, k, {
            get: v,
            enumerable: enumerable,
        });
    });
}

/* arrays */

Array.prototype.repeat = function(n) {
    var res = [];
    for (var i = 0; i < n; i++) {
        res = res.concat(this);
    }
    return res;
}

Array.prototype.memset = function(v, min, max) {
    for (var i = min; i <= max; i++)
        this[i] = v;
    return this;
}

Array.prototype.min = function() {
    var min = Number.POSITIVE_INFINITY;
    forEach(this, function(i, v) {
        if (v < min)
            min = v;
    });
    return min;
}

Array.prototype.max = function() {
    var max = Number.NEGATIVE_INFINITY;
    forEach(this, function(i, v) {
        if (v > max)
            max = v;
    });
    return max;
}

// Gives random array element
// Works for sparse arrays and for arrays with negative indexes
Array.prototype.random = function() {
    var keys = Object.keys(this);
    if (keys.length < 1)
        return undefined;

    var pos = Math.tossIndex(keys.length);
    return this[keys[pos]];
}

/* other stuff */

// expects { 15: obj, 25: obj, 40: obj... }
var TierArray = function(obj) {
    this.tbl = {};
    this.min = Number.POSITIVE_INFINITY;
    var tmp = this;
    forEach(obj, function(k, v) {
        tmp.tbl[k] = v;
        if (k < tmp.min)
            tmp.min = k;
    });
}

TierArray.prototype.get = function(val) {
    var max = this.min;
    forEach(this.tbl, function(k, v) {
        if (k > max && k <= val)
            max = k;
    });
    return this.tbl[max];
}

t.TierArray = function() {
    var x = new TierArray({0: "a", 100: "b"});
    if (x.get(-1) != "a" ||
        x.get(0)  != "a" ||
        x.get(1)  != "a" ||
        x.get(99) != "a" ||
        x.get(100) != "b" ||
        x.get(101) != "b")
        throw new Error("Something is not right");
    return true;
}

function anonymizeStack(stack) {
    var st = stack.split('\n');
    var out = '';
    st.forEach(function(v) {
        out += v
            .replace(/@.*\//, '@') // firefox
            .replace(/\(.*\//, '(') // chrome, IE11
            ;
        out += "\n";
    });
    return out;
}

var stacktraces = {
    firefox: 'ParseStoryString@PATH/save_load.js:1134:19\n',
    chrome: 'URIError: URI malformed\n at decodeURIComponent (<anonymous>)\n at base64decode (PATH/strive/save_load.js:77:12)\n',
    ie11: 'URIError: The URI to be decoded is not a valid encoding\n   at base64decode (PATH/save_load.js:77:5)\n',
};

t.AnonymizeStackTraces = function(name, callback, level) {
    var sub = {};

    forEach(stacktraces, function(k, v) {
        sub[k] = function() {
            var anon = anonymizeStack(v);
            return !anon.match(/PATH/);
        };
    });

    return Test.runTestTree(sub, name, callback, level);
}


code:
index.html
audio/
fonts/
icon/
img/
lib/
movies/
save/
greenworks.js
package.json
Game.rpgproject
steam_api*
errorlog.txt
*.7z
js/.vs/*

repo_name: voidljb/karryn

path: .gitignore


Karryn

note:
<Passive State: 2,5,6,7,8,9,55,107,108,109,154,156,157,158,163,164,168>
<REM NAME ALL>
\REM_DESC[actor_1_name]
</REM NAME ALL>
<REM NAME SCH>
凯瑞恩
</REM NAME SCH>
<REM NAME TCH>
凱瑞恩
</REM NAME TCH>
<Set Sts Data>
TreeType: 10,11,12,13,16,21,22,17,18,19,20
Init Sp: 0
</Set Sts Data>

Tonkin

note:
<REM NAME ALL>
\REM_DESC[actor_6_name]
</REM NAME ALL>
<REM NAME SCH>
汤肯
</REM NAME SCH>
<REM NAME TCH>
湯肯
</REM NAME TCH>

Inmate

note:
<REM NAME ALL>
\REM_DESC[actor_7_name]
</REM NAME ALL>
<REM NAME SCH>
囚犯
</REM NAME SCH>
<REM NAME TCH>
囚犯
</REM NAME TCH>

Guard

note:
<REM NAME ALL>
\REM_DESC[actor_8_name]
</REM NAME ALL>
<REM NAME SCH>
卫兵
</REM NAME SCH>
<REM NAME TCH>
衛兵
</REM NAME TCH>

Yasu

note:
<REM NAME ALL>
\REM_DESC[actor_9_name]
</REM NAME ALL>
<REM NAME SCH>
亚斯
</REM NAME SCH>
<REM NAME TCH>
亞斯
</REM NAME TCH>

Emperor

note:
<REM NAME ALL>
\REM_DESC[actor_10_name]
</REM NAME ALL>
<REM NAME SCH>
皇帝
</REM NAME SCH>
<REM NAME TCH>
皇帝
</REM NAME TCH>

Doctor

note:
<REM NAME ALL>
\REM_DESC[actor_11_name]
</REM NAME ALL>
<REM NAME SCH>
卡吉尔医生
</REM NAME SCH>
<REM NAME TCH>
卡吉爾醫生
</REM NAME TCH>

Aron

note:
<REM NAME ALL>
\REM_DESC[actor_12_name]
</REM NAME ALL>
<REM NAME SCH>

</REM NAME SCH>
<REM NAME TCH>

</REM NAME TCH>

Noinim

note:
<REM NAME ALL>
\REM_DESC[actor_13_name]
</REM NAME ALL>
<REM NAME SCH>

</REM NAME SCH>
<REM NAME TCH>

</REM NAME TCH>

Gobriel

note:
<REM NAME ALL>
\REM_DESC[actor_14_name]
</REM NAME ALL>
<REM NAME SCH>

</REM NAME SCH>
<REM NAME TCH>

</REM NAME TCH>