8
\$\begingroup\$

I have taken my original code from here and turned it into an es6 version of the same game.

Any code review and critiques are useful.

Side note: "Bankrupt" and "Lose a Turn" do not do anything right now as it is a non-multiplayer game right now and I do not think it adds anything to the dynamic, but the wheel I have has those values.

Here is a jsfiddle link to play with https://jsfiddle.net/maniator/4azoqbpv/59/

class OnListener {
  constructor() {
    this.events = {};
  }

  on(event, callback) {
    this.events[event] = callback.bind(this);
  }

  trigger(event, ...value) {
    if (Object.keys(this.events).includes('all')) {
      return this.events['all'](event, ...value);
    }

    for (let _event in this.events) {
      if (event.indexOf(_event) > -1 || _event.indexOf(event) > 1) {
        this.events[_event](event, ...value);
      }
    }
  }
}

class Wheel extends OnListener {
  constructor({
    element,
    button,
    values = []
  }) {
    super();

    this.element = element;
    this.values = values;
    this.modifier = Wheel.spinModifier();
    this.slowdownSpeed = 0.5;
    this.currentRotation = 0;
    this.spinTimeout = null;
    
    const clickFn = this.click.bind(this);

    element.addEventListener('click', clickFn, false);
    button.addEventListener('click', clickFn, false);
  }

  static spinModifier() {
    return Math.random() * 10 + 20;
  }

  stop() {
    clearTimeout(this.spinTimeout);
  }

  click() {
    this.trigger('spin:start');
    return this.spin().then((value) => {
      this.trigger('spin:end', value);

      return value;
    });
  }

  rotate(degrees) {
    this.element.style.transform = `rotate(-${degrees}deg)`;
    this.currentRotation = degrees;
    return this;
  }

  spin(amount = this.currentRotation, modifier = this.modifier) {
    this.stop();
    modifier -= this.slowdownSpeed;
    this.rotate(amount);

    return new Promise((res, rej) => {
      if (modifier > 0) {
        this.spinTimeout = setTimeout(() => {
          res(this.spin(amount + modifier, modifier));
        }, 1000 / 5)
      } else {
        const dataRotation = this.currentRotation;
        const divider = 360 / this.values.length;
        const offset = divider / 2; // half division
        const wheelValue = this.values[Math.floor(Math.ceil((dataRotation + offset) % 360) / divider)];

        this.modifier = Wheel.spinModifier();

        switch (wheelValue) {
          case 0:
            return res(0);
          case -1:
            return res("Free Spin");
          case -2:
            return res("Lose a turn");
          default:
            return res(wheelValue);
        }
      }
    })
  }
}

class Board extends OnListener {
  static randomize(arr) {
    //fisher yates from https://codereview.stackexchange.com/a/12200/3163
    let i = arr.length;
    if (i === 0) return [];
    while (--i) {
      const j = Math.floor(Math.random() * (i + 1));
      const tempi = arr[i];
      const tempj = arr[j];
      arr[i] = tempj;
      arr[j] = tempi;
    }

    return arr;
  }

  constructor({
    answers = [],
    element,
    button
  }) {
    super();

    this.element = element;
    this.answers = Board.randomize(answers);
    this.currentBoard = -1;

    button.addEventListener('click', () => this.next(), false);
  }

  clear() {
    const displayArea = this.element;

    while (displayArea.hasChildNodes()) { //remove old puzzle
      displayArea.removeChild(displayArea.firstChild);
    }
  }

  solve(solution) {
    return (this.answers[this.currentBoard].toUpperCase() === solution.toUpperCase());
  }

  getNextBoard() {
    try {
      const board = this.answers[++this.currentBoard].toUpperCase();
      const boardArray = board.split('');

      this.trigger('new:puzzle', board);

      return boardArray;
    } catch (e) {
      throw new Error('No more levels!');
    }
  }

  inputLetter(_letter) {
    let count = 0;

    this.letters.forEach((obj) => {
      const {
        letter,
        element
      } = obj;
      if (!obj.seen && _letter === letter) {
        element.textContent = letter;
        obj.seen = true;
        count += 1;
      }
    });

    return count;
  }

  next() {
    const boardArray = this.getNextBoard();
    const displayArea = this.element;

    this.clear();

    this.letters = [];

    let word = document.createElement('div');

    word.classList.add('word');

    const words = boardArray.reduce((words, currentLetter, index) => {
      const letter = document.createElement('div');
      letter.classList.add('wordLetter');
      letter.id = `letter_${index}`;

      if (currentLetter !== ' ') {
        letter.classList.add('letter');
        this.letters.push({
          letter: currentLetter,
          element: letter
        });

        word.appendChild(letter);
      } else {
        words.push(word);
        word = document.createElement('div');
        word.classList.add('word');
      }

      return words;
    }, []);

    // push the last one
    words.push(word);

    words.forEach((wordElement) => {
      displayArea.appendChild(wordElement);
    });
  }
}

class Game {
  constructor({
    wheel,
    board,
    vowelButton,
    solveButton,
    moneyElement
  }) {
    this.wheel = wheel;
    this.board = board;
    this.vowels = ['A', 'E', 'I', 'O', 'U'];
    this.totalScore = 0;
    this.moneyElement = moneyElement;

    vowelButton.addEventListener('click', () => {
      if (this.totalScore >= 100) {
        this.totalScore -= 100;
        this.guessLetter(0, true);
      } else {
        // @todo an else case
      }
    }, false);

    solveButton.addEventListener('click', () => {
      const solution = prompt('What is the solution?');
      if (board.solve(solution)) {
        solution.split('').forEach((letter) => {
          this.board.inputLetter(letter.toUpperCase())
        });
        setTimeout(() => alert('Puzzle solved!'), 10);
      } else {
        alert('Puzzle not solved!');
      }
    }, false);
  }

  start() {
    this.listen();
    this.board.next();
  }

  guessLetter(value, asVowel = false) {
    const letter = prompt(`Guess a ${asVowel ? 'vowel' : 'letter'}`);

    try {
      if (!(/^[a-zA-Z]$/.test(letter))) {
        throw new Error('Must input an actual letter A-Z');
      }

      const capitalLetter = letter.toUpperCase();
      const letterIsVowel = this.vowels.includes(capitalLetter);

      if (asVowel && !letterIsVowel) {
        throw new Error(`Must input a vowel: [${this.vowels.join(', ')}]`);
      } else if (!asVowel && letterIsVowel) {
        throw new Error(`Cannot input a vowel: [${this.vowels.join(', ')}]`);
      }

      const count = this.board.inputLetter(capitalLetter);

      console.log(`Found ${letter} ${count} times worth $${value * count}`);

      if (!asVowel) {
        this.totalScore += (value * count);
      }

      this.moneyElement.textContent = this.totalScore;
    } catch (error) {
      alert(error.message);
      this.guessLetter(...arguments);
    }
  }

  listen() {
    this.wheel.on('spin:end', (event, value) => {
      if (isNaN(value) || value === 0) {
        console.log('SPUN SOMETHING BAD', value);
      } else {
        console.log('CAN GUESS A LETTER', value);
        this.guessLetter(value);
      }
    });
    this.board.on('new:puzzle', (event, ...value) => {
      console.log(`${event} ${value}`);
      this.wheel.stop();
    });
  }
}

const wheel = new Wheel({
  element: document.getElementById('wheel'),
  values: [5000, 600, 500, 300, 500, 800, 550, 400, 300, 900, 500, 300, 900, 0, 600, 400, 300, -2, 800, 350, 450, 700, 300, 600],
  button: document.getElementById('spin'),
});

const board = new Board({
  element: document.getElementById('display'),
  answers: [
    "doctor who", "the dark knight rises", "wheel of fortune",
    "facebook", "twitter", "google plus", "sea world", "pastrami on rye",
    "i am sparta", "whose line is it anyway", "google chrome"
  ],
  button: document.getElementById('newpuzzle')
});

const game = new Game({
  wheel,
  board,
  vowelButton: document.getElementById('vowel'),
  solveButton: document.getElementById('solve'),
  moneyElement: document.getElementById('money')
});

game.start();

console.log(game);
@import url(//fonts.googleapis.com/css?family=Changa+One);
#game {
  font-family: 'Changa One' serif;
}

#wheel {
  top: 0;
  left: 0;
  width: 500px;
  -moz-border-radius: 250px;
  -webkit-border-radius: 250px;
  border-radius: 250px;
  -webkit-transition: -webkit-transform 0.5s linear;
  -moz-transition: -moz-transform 0.5s linear;
  -opera-transition: -opera-transform 0.5s linear;
  -ms-transition: transform 0.5s linear;
}

#tick {
  margin-left: 244px;
  font-family: Arial;
  font-size: 14pt;
}

#display {
  background-color: #43759f;
  padding: 1em 2em;
  text-align: center;
  border-radius: 40px;
  border: 4px double #fff;
}

#display div.word {
  display: inline-block;
}
#display div.word:not(:first-child) {
  padding: 0 30px;
}

#display div.wordLetter {
  display: block;
  width: 50px;
  height: 65px;
  margin: 2px;
  border: 2px solid #43759f;
  float: left;
  line-height: 65px;
  font-size: 40px;
}

#display div.wordLetter.letter {
  border-color: #eee;
}

#playArea {
  clear: both;
}

#playArea button {
  text-transform: uppercase;
  z-index: 1000;
  border: 4px double #fff;
  background-color: #43759f;
  padding: 6px 14px;
  color: white;
  font-family: inherit;
  font-size: 12pt;
}

#playArea #moneyArea {
  float: right;
  position: relative;
  padding: 10px;
  border: 2px black solid;
  width: 200px;
  margin: 5px;
  font-size: 14pt;
}

.clear {
  clear: both;
}
<div id="game">
    <div id="display"></div>
    <div id="playArea">
        <button id="spin">spin wheel</button>
        <button id="vowel">buy vowel</button>
        <button id="solve">solve puzzle</button>
        <button id="newpuzzle">new puzzle</button>
        <div id="moneyArea">
            Score: $<span id="money">0</span>
        </div>
    </div>
    <div class="clear"></div>
    <div id="tick">⇩</div>
    <img id="wheel" src="//i.imgur.com/R7JYazp.png" data-rotation="0" />
</div>

\$\endgroup\$
9
  • \$\begingroup\$ I've never seen this form before constructor({ element, button, values = [] }). What is that? Doesn't look valid to me. \$\endgroup\$
    – Xotic750
    Sep 23, 2016 at 11:59
  • \$\begingroup\$ That is valid es6 and also now valid JavaScript @Xotic750 :-) Otherwise none of it would work \$\endgroup\$
    – Naftali
    Sep 23, 2016 at 12:14
  • \$\begingroup\$ Can you point me at some documentation? I though perhaps it was new with default parameters, but I don't see this pattern there. And my linter configured for ES6 also points at this construction. \$\endgroup\$
    – Xotic750
    Sep 23, 2016 at 12:19
  • \$\begingroup\$ Ok I have found it. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… \$\endgroup\$
    – Xotic750
    Sep 23, 2016 at 13:20
  • \$\begingroup\$ Ha sorry I was on a train @Xotic750 Yea it is object destructuring :-) \$\endgroup\$
    – Naftali
    Sep 23, 2016 at 13:54

1 Answer 1

1
\$\begingroup\$

In no particular order:

  • the OnListener class should be called EventEmitter, that's the known convention (and it is also used by Node).
  • Your this.events should be a Map, then you can iterate it with for..of:

      trigger(event, ...value) {
        if (this.events.has('all')) {
          return this.events.get('all')(event, ...value);
        }
    
        for (let [name, fn] of this.events) {
          if (event.includes(name) || name.includes(event)) {
            fn(event, ...value);
          }
        }
      }
    
  • On that note, if you have an event called 'all' the trigger() function will return early, and the actual event won't be triggered.

  • return Math.random() * 10 + 20; magic numbers are magic.

  • this.spinTimeout = setTimeout(() => ..., 1000 / 5) more magic numbers

  • const wheelValue = this.values[Math.floor(Math.ceil((dataRotation + offset) % 360) / divider)]; I don't even know how to read that, let alone what it means.

  • Your static randomizer function can be implemented as an iterator function, for better readability:

    static function* randomize(arr) {
      let copy = [...arr]; // don't modify the original!
      while (copy.length > 0) {
        const randIndex = ~~(Math.random() * copy.length);
        yield copy.splice(randIndex, 1)[0]; // remove random element and yield it.
      }
    }
    

    Then use it at either for..of or like so: this.answers = [...Board.randomize(answers);]

  • When you see this:

    let count = 0;
    
    this.letters.forEach((obj) => {
        // somewhere inside
        count += 1;
      }
    });
    
    return count;
    

    You want reduce.

  • This catch (error) { // snip this.guessLetter(...arguments); } means I can't drop the guess by pressing "Cancel".

  • #playArea #moneyArea two IDs are redundant. #moneyArea should do just fine.

The more general things

  • Both your Game class and your Board class have presentation mixed in their logic. (They both access the DOM and do processing)
  • Having just events is a good way to define what a component can or cannot do. Having just methods is a good way to make the component readable and clear. Having both is a mess.
  • Using Exceptions as flow control is a Bad ThingTM, it's akin to using Ye Olde goto Of The Dark Times.
\$\endgroup\$
3
  • \$\begingroup\$ I know about the exception part. I have not come up with a good way to handle errors are show the prompts etc (using native is inherently "bad". The HTML/CSS was copied over from my old example and I did not really change it much, but thank you for going over it :-) . So Can the "randomize" function you created be called getNextPuzzle? Also I thought yield etc were not fully fleshed out in es6? \$\endgroup\$
    – Naftali
    Sep 23, 2016 at 15:38
  • \$\begingroup\$ Iterators are fully in the spec in ES6. you can see it in action here: jsfiddle.net/ypgs707j \$\endgroup\$ Sep 23, 2016 at 15:39
  • \$\begingroup\$ Nice catch on the all bug! I did not see that one! \$\endgroup\$
    – Naftali
    Sep 23, 2016 at 15:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.