简体   繁体   中英

Storing tree data in Javascript

I need to store data to represent this:

Water + Fire = Steam
Water + Earth = Mud
Mud + Fire = Rock

The goal is the following: I have draggable HTML divs, and when <div id="Fire"> and <div id="Mud"> overlap, I add <div id="Rock"> to the screen. Ever played Alchemy on iPhone or Android? Same stuff

Right now, the way I'm doing this is a JS object :

var stuff = {
    'Steam' : { needs: [ 'Water', 'Fire'] },
    'Mud'   : { needs: [ 'Water', 'Earth'] },
    'Rock'  : { needs: [ 'Mud',   'Fire'] },
    // etc...
};

and every time a div overlaps with another one, I traverse the object keys and check the 'needs' array.

I can deal with that structure but I was wondering if I could do any better?

Edit : I should add that I also need to store a few other things, like a short description or an icon name. So typicall I have Steam: { needs: [ array ], desc: "short desc", icon:"steam.png"},

Final edit : thank you everyone for contributing, I found really valuable input in all your comments

Whenever you create a data structure you need to keep the following things in mind:

  1. There entropy should be high (ie there should be less redundancy).
  2. The access time of an element should be less.

The way I see it a tree is not the best way to represent the data you're trying to model.

However what you've demonstrated in your code is good. Create an object whose properties are the right hand side elements of your equations:

var stuff = {
    Steam: ...,
    Mud: ...,
    Rock: ...
};

This allows the elements to be accessed in O(1) time.

The left hand side of your equations can be modeled as arrays as you have done. That's what I would have done.

However wrapping them in an extra object is just extra redundancy, and it increases the access time of the elements.

I would have modeled it like this:

var stuff = {
    Steam: ["Water", "Fire"],
    Mud: ["Water", "Earth"],
    Rock: ["Mud", "Fire"]
};

You may then normalize your table by replacing all occurances of composite "stuff" in the arrays by their respective elementary "stuff".

Edit: From your comments I suggest you use a data structure as follows (you may and should store it in a .json file):

{
    "Water": {
        "needs": []
    },
    "Fire": {
        "needs": []
    },
    "Earth": {
        "needs": []
    },
    "Steam": {
        "needs": ["Water", "Fire"]
    },
    "Mud": {
        "needs": ["Water", "Earth"]
    },
    "Rock": {
        "needs": ["Water", "Earth", "Fire"]
    }
}

The reason you should store the elementary stuff and not composite stuff is that the same stuff may be made by more than one combination of composite stuff. Storing the constituents in elementary form is the least redundant.

How about coding directly the elements with the other dependencies and what them produce?

var elements = {
  water: {
    earth: 'mud', //water with earh produces mud
    fire: 'steam'
  },
  fire: {
    water: 'steam',
    mud: 'rock'
  },
  earth: {
    water: 'mud'
  },
  mud: {
    fire: 'rock'
  }
}

So then when you have #div1 and #div2 you just do:

elements[div1][div2]
elements['fire']['water']
"steam"

and you get the id of the produced element.

I've played Alchemy, I would replace the needs array with links to the needed objects. I have also created a makes array which I think you would need in the game. Setting up this deep linking once makes subsequent lookups much faster.

var stuff = {
  Fire: {
    name: 'Fire', active: true, needs: []
  }
  ,Water: {
    name: 'Water', active: true, needs: []
  }
  ,Earth: {
    name: 'Earth', active: true, needs: []
  }
  ,Steam: {
    name: 'Steam', active: false, needs: ['Water','Fire']
  }
  ,Mud: {
    name: 'Mud', active: false, needs: ['Water','Earth']
  }
  ,Rock: {
    name: 'Rock', active: false ,needs: ['Mud','Fire']
  }
};

for (var name in stuff) {
    // create links for needs and wants
    for (var i=0, n; n = stuff[name].needs[i]; i++) {
        if (stuff[n]) {
            stuff[name].needs[i] = stuff[n];
            if (!stuff[n].makes) stuff[n].makes = [];
            stuff[n].makes.push(stuff[name]);
        }
    }
    (function (o) {
        o.getNeeds = function () {
            var needs = [];
            for (var i=0, n; n = o.needs[i]; i++) {
                needs.push(o.needs[i].name);
            }
            return needs;
        };
        o.getMakes = function () {
            var makes = [];
            if (!o.makes) o.makes = [];
            for (var i=0, n; n = o.makes[i]; i++) {
                makes.push(o.makes[i].name);
            }
            return makes;
        };
        o.dump = function () {
            return o.name + " needs(" + o.getNeeds().join(',') + "), makes(" + o.getMakes().join(',') + ")";
        };
    })(stuff[name]);
}

stuff.testCombine = function (itemArray) {
    // itemArray is an unordered array of "stuff" names to test, eg ['Water','Fire']
    // if the elements in itemArray match an makes list for an item, this function returns that item.
    // if no combine was found, this function returns false
    if (!itemArray || !itemArray[0] || !stuff[itemArray[0]]) return false;

    // itemArray[0] is the guinea pig item, we see what it can make, and then see what the ingredient lists are and compare them to itemArray
    possible = stuff[itemArray[0]].makes;
    itemArray = itemArray.sort();

    for (var i=0, p; p = possible[i]; i++) {
        var n = p.getNeeds().sort();

        var matched = false;
        // check if n and itemArray are identical
        if (n.length && n.length == itemArray.length) {
          var j = 0;
          for (j=0; j < n.length && n[j] == itemArray[j]; j++);
          if (j == n.length) matched = true;
        }

        if (matched) return p;
    }
    return false;
}

// shows properties of Steam
alert(stuff.Steam.dump());
// shows properties of Water
alert(stuff.Water.dump());

alert("Water can be used to make :\n" + [stuff.Water.makes[0].dump(), stuff.Water.makes[1].dump()].join("\n"));

// stuff.Steam.needs[0] is Water, .makes[1] is Mud, .makes[0] is Rock
alert(stuff.Steam.needs[0].makes[1].makes[0].name);

// test if 'Water', 'Earth' makes something:
var m = stuff.testCombine(['Water','Earth']);
if (!m) { 
    alert('Did not Combine'); 
} else {
    alert('Combined to make ' + m.dump());
}

If you don't mind including another external library and are comfortable with LINQ, you can use linq.js for this.

var stuff = {
  'Steam' : { needs: ['Water', 'Fire'] },
  'Mud'   : { needs: ['Water', 'Earth'] },
  'Rock'  : { needs: ['Mud',   'Fire'] }
    // etc...
};

function Alchemy(stuff) {
  var recipes = 
    Enumerable.From(stuff).ToLookup(
      "$.Value.needs",
      "$.Key", 
      "Enumerable.From($).OrderBy().ToString('+')"
    );

  this.attempt = function(elem1, elem2) {
    return recipes.Get([elem1, elem2]).ToString();
  };
};

var alchemy = new Alchemy(stuff);
console.log(alchemy.attempt('Fire', 'Mud'));   // "Rock"
console.log(alchemy.attempt('Fire', 'Earth')); // ""
console.log(alchemy.attempt('Fire', 'Water')); // "Steam"

Notes

  • Enumerable.From(stuff) splits your stuff object into its Key and Value parts.
    For instance, Key would refer to "Rock" and Value to { needs: ['Mud', 'Fire'] } .
  • ToLookup() creates a look-up dictionary from that. It takes 3 parameters:
    1. what to look for (in this case, the elements in "$.Value.needs" )
    2. what to return if a match was found (in this case, the resulting element's name, ie the Key )
    3. the transformation function that creates the dictionary key (in this case an array of ingredients is transformed into a sorted string: ['Mud', Fire'] becomes "Fire+Mud" ).
  • the Get() function finds a match for its argument, using the same transformation function.

Note that a string argument like "$.Value.needs" is a shorthand for
function ($) { return $.Value.needs; } function ($) { return $.Value.needs; } .

linq.js also provides many more useful functions that can transform complex tasks into one-liners.


Edit: Returning all the additional info from the lookup would be as simple as:

function Alchemy(stuff) {
  var recipes = 
    Enumerable.From(stuff).ToLookup(
      "$.Value.needs",
      null, // return the object unchanged 
      "Enumerable.From($).OrderBy().ToString('+')"
    );

  this.attempt = function(elem1, elem2) {
    return recipes.Get([elem1, elem2]).FirstOrDefault();
  };
};

console.log(alchemy.attempt('Fire', 'Mud')); 
/* result
{
  Key: "Rock",
  Value: {
    needs: ["Mud", "Fire"],
    whatever: "else you had defined in {stuff}"
  }
}
*/

The purpose of the Lookup object is to increase speed. You could also traverse the entire object graph every time:

function alchemy(elem1, elem2) {
  return 
    Enumerable
    .From(stuff)
    .Where(function ($) {
      var recipe = Enumerable.From($.Value.needs);
      return recipe.Intersect([elem1, elem2]).Count() == 2;
    })
    .Select("{element: $.Key, properties: $.Value}")
    .FirstOrDefault();
);

console.log(alchemy('Fire', 'Water'));
// {element: "Steam", properties: {needs: ["Water", "Fire"]}}

Note that .Select() is optional. You could remove it, in which case the result would be the same as in the previous example.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM