Monday, October 1, 2012

Iterating JavaScript for Kids

‹prev | My Chain | next›

After some messing about, I think the follow code ought to suffice to remove letter "meteors" from my learn-to-type Three.js game:
  function removeMeteor(number) {
    var head = meteors.slice(0, number)
      , tail = meteors.slice(number + 1);

    scene.remove(meteors[number].sprite);
    meteors = head.concat(tail);
  }
Given an index of the array, I slice from the beginning of the array up to, but not including the element at the given index (this is how slice() works), assigning the result to the "head" of the array. I then slice from the element after the supplied index to the end of the array, assigning the result to the "tail" of the array. The array with the indicated element removed is then the concatenation of the head and tail. I update the global variable accordingly and remove the corresponding Three.js Sprite from the scene.

I am trying to not care too much about global variables in Gaming JavaScript. I do not think the discussion really adds to kids' understanding of things. If I was concerned, I could have made this a method of Array. Then again, the necessity to remove the sprite as well would likely force me to create a Meteors class as a subclass of Array that could perform both operations. Yuck. Especially since I am trying my best to keep this conceptually easy for kids.

To decide which meteor needs removing, I make another compromise:
  document.addEventListener('keypress', function(event) {
    var letter_pressed = String.fromCharCode(event.keyCode);
    
    for (var i=0; i<meteors.length; i++) {
      if (letter_pressed != meteors[i].letter) continue;
      removeMeteor(i);
      break;
    };
  });
The compromise here is the use of a for-loop. Call me crazy, but I had hoped to stick with array methods in Gaming JavaScript. Callbacks in JavaScript are a way of life. Even in this example I am already using an event callback, so an iterator callback hardly constitutes added complexity. And for-loops are just ew.

Unfortunately, I cannot get away with a forEach() in this case:
  document.addEventListener('keypress', function(event) {
    var letter_pressed = String.fromCharCode(event.keyCode);
    
    meteors.forEach(function(meteor, number) {
      if (letter_pressed != meteor.letter) return false;
      removeMeteor(number);
      return true;
    });
  });
The problem is that this code will not stop after the first meteor is removed. If there are multiple "f" meteors falling, I will end up destroying multiple meteors with a single shot. Worse yet is that the the meteors array shrinks with a hit, meaning that a subsequent removal could be for an out-of-bounds entry in the array.

I could use some():
    meteors.some(function(meteor, number) {
      if (letter_pressed != meteor.letter) return false;
      removeMeteor(number);
      return true;
    });
The some() method will stop after the first true return value from the callback. But of course, some() makes no semantic sense in this instance. It is meant to be used in a boolean context and I am completely ignoring the return value.

The filter() method almost works, but I would have to add a tracking variable to get the original indexes so that I could remove the first hit:
    var hits = meteors.
          filter(function(meteor) {
            return (letter_pressed == meteor.letter);
          });
I might just as well stick with a for-loop, which is what I do. Bother.

With that more or less settled, all that remains in the game is to keep score and to increase the difficulty level. Scoring is a simple matter of incrementing a score integer whenever the correct letter is typed:
  document.addEventListener('keypress', function(event) {
    var letter_pressed = String.fromCharCode(event.keyCode);
    
    for (var i=0; i<meteors.length; i++) {
      if (letter_pressed != meteors[i].letter) continue;
      score++;
      removeMeteor(i);
      break;
    };
  });
To keep things simple, increasing the difficulty level will only involve new letters:
  function nextLetter() {
    var pool = ['f', 'j'];
    if (score > 10) pool = pool.concat(['d', 'k']);
    if (score > 20) pool = pool.concat(['s', 'l']);
    if (score > 30) pool = pool.concat(['a', 'r', 'u']);
    
    return pool[Math.floor(Math.random() * pool.length)];
  }
After the player has mastered the touch-typing "home keys", new keys are added. I can leave all letters as an exercise for the kids following along. That seems like a decent stopping point for this game. I have one more game / simulation that I would like to include in the book. I will pick up with that tomorrow.

Editable code


Day #527

3 comments:

  1. If the letter is the "primary key" for the meteor, why not use an object instead of an array and have the index be the letter? You can then just index in via the key pressed. And you can still "for (k in meteors)" an object.

    ReplyDelete
    Replies
    1. oh, and then removal is simply "delete meteors[k]"

      Delete
    2. That would work nicely, but there can be multiples of the same letter falling at any given time. If I were doing this up right, the first level would increase in difficulty by increasing the frequency of letters and the speed at which they fall. So it might be possible to see a half dozen "f" and "j" letters falling.

      Delete