10
\$\begingroup\$

This is a 2 player (Will soon have AI in it - I am not asking for help with AI) game with mechanics similar to the card game named Magic the Gathering. Bear with me, this is a lot of code. If you feel like you need to review one file at a time, do so.

The code is hosted here if you wish to see how it works.

When you first open the game, it should look something like this:

This

That is the starting screen. To begin your turn, select a card by clicking a card name on the left hand side but closest to the "Turn: Angels". When you do so, the name should become a bit brighter. This means that the card is selected. You can now attack the enemy card by clicking on their name.

Cards can only attack once per turn. Cards can only attack if you have more mana then the mana cost (Mana is the blue bars at top left and right, a cards mana cost is the blue rectangle bottom left below the health) A cards attack is shown by the red rectangle directly below the health bar. A card that has a number at the blue rectangle on the right generates mana. This is mostly limited to land.

Here are the relevant files:

┊
├Angels-And-Demons
│ ├data
│ │ ├DarkHand.js
│ │ ├LightHand.js
│ │ └styles.css
│ ├lib
│ │ └vue.js
│ ├src
│ │ ├BaseClasses.js
│ │ ├components.js
│ │ ├Dark.js
│ │ ├expandWeights.js
│ │ ├Land.js
│ │ ├Crippler.js
│ │ └Light.js
│ └game.html
┊

And here are the files:

data/DarkHand.js

var enemyWeights = [
  [Cuthulu, 1],
  [Demon, 2],
  [Ravine, 20],
  [HellHound, 3],
  [ThornKnight, 3],
  [DarkAgent, 3]
];

data/LightHand.js

var playerWeights = [
  [EternalFlame, 1],
  [Angel, 2],
  [Mountain, 20],
  [Paladin, 3],
  [Priest, 3],
  [Lamp, 3]
];

data/styles.css

body {
  background-color: #222222
}
div {
  display: inline-block;
}
.discard, .summonOrAttack {
  color: White;
}
.manaBar  {
   height:20px;
   background-color: Blue;
   color: White;
   text-align: center;        
}
.name {
  width: 80%;
}
.tagList {
  width:100%;
  margin: 4 auto;
}
.tagList * {
  width: 20%; 
  height:20px;
  color: White;
  padding: 2%;
  border-radius:4px;
  text-align: center;
}
.tagList .manaCost {
   background-color: MediumBlue;
}
.tagList .attack {
  background-color: DarkRed;
}
.tagList .manaPerRound {
  background-color: Blue;
}
table {
  display: inline-table;
  width: 100%;
}
.health {
  height: 20px;
  text-align: center;
}
#playerHand, #playerUnits, #enemyUnits, #enemyHand  {
  width: 20%;
  text-align: center;
}
.card {
  text-align: center;
  width: 100%;
}
#turnStats, #turn {
  color: White;
  text-align: center;
}

lib/vue.js

A local copy of vue so that I could work on it offline.

src/BaseClasses.js

class TurnManager {
  constructor() {
    this.turnNumber = -1;
  };
  nextTurn() {
    this.turnNumber++;
  };
};
class Effect {
  constructor(value, turns, name, netValueWhenDone) {
    Object.assign(this, {
      value: value,
      turns: turns,
      name: name,
      netValueWhenDone: netValueWhenDone
    });
  }
  apply(card) {
    card.activeEffects[this.name] = {}
    Object.assign(card.activeEffects[this.name], {
       value: this.value,
       turns: this.turns,
       netValueWhenDone: this.netValueWhenDone,
       remainingTurns: this.turns,
       name: this.name
    })
  }
}
class Deck {
  constructor(isHand, manaManager, weights) {
    var cards = {}
    Object.assign(this, {
      hasHadCardAdded: false,
      cards: Array(10).fill().map( () => new BlankCard ),
      currentId: 1,
      isHand: isHand,
      manaManager: manaManager,
      selectedCardID: -1,
      weights: weights ? expandWeights(weights) : undefined
    });
    if (manaManager) {
      this.hand = this.isHand ? this : this.manaManager.deck;
      this.deck = this.isHand ? this.manaManager.deck : this;
    };
  };
  sealCards() {
    Object.seal(this.cards);
  };
  updateEffects() {
    this.cards.forEach( (card) => {
      Object.values(card.activeEffects).forEach( (effect) => {
        let isNew = effect.turns === effect.remainingTurns
        if( isNew ) {
          card[effect.name] += effect.value;
        } else if ( effect.remainingTurns === 0 ) {
          card[effect.name] -= effect.value - effect.netValueWhenDone;
          delete card.activeEffects[effect.name]
        };
        effect.remainingTurns--;
      });
    });
  };
  addCardFromWeights() {
    var newCard = getRandomItem(this.weights);
    this.addCards(new newCard(this.hand, this.deck));
    this.weights.splice(this.weights.indexOf(newCard), 1);
  };
  get selectedCard() {
    return this.cards[this.selectedCardID];
  };
  enableEnemyDeck() {
    this.enemyDeck.ArrayOfCardIDs.forEach((cardID) => {
      this.enemyDeck.cards[cardID].locked = false;
    });
  };
  disableEnemyDeck() {
    this.enemyDeck.ArrayOfCardIDs.forEach((cardID) => {
      this.enemyDeck.cards[cardID].locked = true;
    });
  };
  get ArrayOfCards() {
    return Object.values(this.cards);
  };
  get ArrayOfCardIDs() {
    return Object.keys(this.cards);
  };
  attack() {
    // the opponent is always the person who attacks.

    var opponentCardID = this.enemyDeck.selectedCardID,
      opponentCard = this.enemyDeck.selectedCard,
      yourCardID = this.selectedCardID,
      yourCard = this.selectedCard;

    if (yourCardID + 1 && opponentCardID + 1 && !opponentCard.used && !yourCard.isLand && !yourCard.isPrimal) {
      opponentCard.health -= (yourCard.attack === "N/A" ? 0 : yourCard.attack);
      yourCard.health -= (opponentCard.attack === "N/A" ? 0 : opponentCard.attack);
      Object.assign(opponentCard, {
        used: true,
        selected: false
      });
      if( opponentCard.effects ) {
        Object.values(opponentCard.effects).forEach( (item) => {
          item.apply(yourCard)
        })
      }
      this.enemyDeck.selectedCardID = -1;
      this.selectedCardID = -1;
      this.enemyDeck.manaManager.mana -= opponentCard.manaCost;
      this.ArrayOfCards.forEach((card) => {
        if (card.health <= 0 && card.health !== null) {
          this.removeCards(card);
        };
      });
      if( this.hasHadCardAdded && this.ArrayOfCards.every( card => (card instanceof BlankCard || card instanceof Land)) ) {
        this.enemyDeck.win()
      }
    };
  };
  win() {
    if(this === enemyDeck ) {
      alert("Enemy wins!");
    } else if ( this === playerDeck ) {
      alert("Player wins!");
    } else {
      alert("Cat?")
    };
    gameOver = true;
  };
  addCards(...cards) {
    if( cards.some( (card) => {
      return !(card instanceof Land) && !(card instanceof BlankCard)
      })) {
      this.hasHadCardAdded = true;
    };
    var emptyCardIDs = this.cards.filter( item => item.name === null ).map( item => this.cards.indexOf(item));
    cards.forEach( (card) => {
      this.isHand ? card.inHand = true : card.inHand = false;
      card.ID = emptyCardIDs[0];
      this.cards[emptyCardIDs[0]].propogate(card);
      emptyCardIDs.shift();
    });
  };
  removeCards(...cards) {
    cards.forEach((card) => {
       this.cards[card.ID].propogate(new BlankCard);
    });
  };
  Lockdown(...cards) {
    this.ArrayOfCardIDs.forEach((cardID) => {
      this.cards[cardID].locked = true;
    });
    cards.forEach((item) => {
      this.cards[item.ID].locked = false;
    });
  };
  OpenUp() {
    this.ArrayOfCardIDs.forEach((cardID) => {
      this.cards[cardID].locked = false;
    });
  };
};
class Card {
  constructor(maxHealth, attack, nameColor, manaCost, name, inHand, hand, deck, manaPerTurn) {
    Object.assign(this, {
      maxHealth: maxHealth,
      health: maxHealth,
      attack: attack,
      name: name,
      nameColor: nameColor,
      inHand: inHand,
      deck: deck,
      manaCost: manaCost,
      manaManager: deck.manaManager,
      manaPerTurn: manaPerTurn,
      hand: hand,
      decks: [playerDeck, enemyDeck],
      selected: false,
      locked: false,
      used: false,
      activeEffects: {}
    });
  };
  discard() {
    if( this.isDecksTurn ) {
      if( confirm("Are you sure you wish to discard this card?") ) {
        if( this.inHand ) {
          this.hand.removeCards(this);
        } else {
          this.deck.removeCards(this);
        };
      };
    };
  };
  copy() {
    return Object.setPrototypeOf(Object.assign({}, this), this.__proto__);
  };
  get indexInDecks() {
    return this.decks.indexOf(this.deck);
  };
  get isDecksTurn() {
    // true means it is... and false means it is not.
    return (turnManager.turnNumber % 2 === this.decks.indexOf(this.deck));
  }
  get isLand() {
    return this instanceof Land;
  }
  get isPrimal() {
    return this instanceof Primal;
  }
  onclick() {
    if (!this.used) {
      if (this.isDecksTurn) {
        if (this.manaManager.mana >= (this.manaCost === "N/A" ? 0 : this.manaCost) && !this.isLand) {
          if (this.isPrimal) {
            enemyWins();
          } else if (!this.selected) {
            this.toggleSelected()
            this.deck.Lockdown(this);
            this.deck.enableEnemyDeck();
            this.deck.selectedCardID = this.ID;
          } else if (this.selected) {
            this.toggleSelected();
            this.deck.OpenUp();
            this.deck.disableEnemyDeck();
            this.deck.selectedCardID = -1;
          };
        };
      } else if (!this.isDecksTurn && this.deck.enemyDeck.selectedCardID + 1) {
        this.deck.selectedCardID = this.ID;
        this.deck.enemyDeck.OpenUp();
        this.deck.attack();
      };
    };
  };
  propogate(card) {
    Object.assign(this, card);
    Object.setPrototypeOf(this, card.__proto__)
  }
  summon() {
    if (this.summonCost <= this.manaManager.mana && turnManager.turnNumber % 2 === this.decks.indexOf(this.deck)) {
      if (confirm('Are you sure you want to summon this card?')) {
        this.hand.manaManager.mana -= this.summonCost === "N/A" ? 0 : this.summonCost;
        Object.assign(this, {
          inHand: false,
          used: true // summoning sickness
        });
        this.deck.addCards(this.copy());
        this.hand.removeCards(this);
      };
    };
  };
  toggleSelected() {
    Object.assign(this, {
      selected: !this.selected
    });
  };
  get style() {
    return `width: ${Math.floor(this.health / this.maxHealth * 100)}; background-color: ${this.barColor};`;
  };
  get barColor() {
    var r = 255 - (this.health / this.maxHealth) * 255;
    var g = (this.health / this.maxHealth) * 255;
    return `rgb(${Math.floor(r)}, ${Math.floor(g)}, 0)`;
  };
  get id() {
    return this.ID;
  };
};
class BlankCard extends Card {
  constructor() {
    super(null,null,null,null,null,null,null,{manaManager:null},null,null)
  }
}
class ManaManager {
  constructor(deck) {
    this.mana = 0;
    this.deck = deck;
  };
  get maxMana() {
    return 20 + this.deck.ArrayOfCards.map((i) => (i instanceof Land ? i.manaPerTurn : 0)).reduce((totalManaPerTurn, cardManaPerTurn) => {
      return totalManaPerTurn + cardManaPerTurn;
    });
  };
  get manaBarWidth() {
    return Math.floor(this.mana / this.maxMana * 100);
  };
  get manaPerTurn() {
    var result = this.deck.ArrayOfCards.reduce((accumulator, card) => {
      return accumulator + (card.manaPerTurn === "N/A" ? 0 : card.manaPerTurn);
    }, 0);
    return result;
  };
  set manaGain(mana) {
    this.mana += mana;
    if (this.mana > this.maxMana) {
      this.mana = this.maxMana;
    };
  };
};
class Primal extends Card {
  constructor(hand, deck, name) {
    super(null, "N/A", "#DD00DD", 30, name, true, hand, deck, "N/A");
    this.summonCost = 0;
  };
};
class Land extends Card {
  constructor(manaPerTurn, name, nameColor, inHand, hand, deck) {
    super(null, "N/A", nameColor, "N/A", name, deck.isHand, hand, deck, manaPerTurn);
    this.summonCost = 0;
  };
};

src/components.js

Vue.component('card', {
  'template': `<div class="card" v-if="item.name !== null" :style="{
                  'border-radius': '4px',
                  'background-color': item.selected ? 'LightGrey' : 'transparent'
                  }"> 
                 <div class="name">
                   <div class="tag" :style="{
                     'cursor': !item.locked && !gameOver && !item.used && ( (item.inHand ? item.summonCost : item.manaCost) <= item.manaManager.mana) && (item.isDecksTurn) && (item.inHand ? true : !item.isLand) ? 'pointer' : 'default',
                     'text-shadow': item.selected ? ('0 0 4px' + item.nameColor) : 'none',
                     'width': '100px',
                     'color': (item.locked  || item.used || gameOver || ( (item.inHand ? item.summonCost : item.manaCost) > item.manaManager.mana) || (item.isDecksTurn) && (item.inHand ? false : item.inHand)) ? (colors.DarkerColors[item.nameColor]) : ((item.selected) ? (colors.LighterColors[item.nameColor]) : (item.nameColor))}" align="center" :onclick=" gameOver ? void(0) : ((item.indexInDecks ? 'enemy' : 'player') + (item.inHand ? 'Hand' : 'Deck')+ '.cards['+item.ID+']' + (item.inHand ? '.summon()' : '.onclick()'))">
                         {{ item.name }}
                   </div>
                   <div class="discard" :style="{'cursor': item.isDecksTurn ? 'pointer' : 'default' }" :onclick="(item.indexInDecks ? 'enemy' : 'player') + (item.inHand ? 'Hand' : 'Deck')+ '.cards['+item.ID+']' + '.discard()'"> D </div>
                 </div> 
                 <br v-if="item.health && !item.inHand"> 
                 <div v-if="item.health && !item.inHand" style="background-color:black; width:100px;" class="health"> 
                   <div :style="item.style" :title="'Health: ' + item.health +'/' + item.maxHealth">{{item.health}}</div> 
                 </div> 
                 <br> 
                 <div class="tagList"> 
                   <div class="manaCost" :title="(item.inHand ? 'Summon cost: ' + item.summonCost : 'Usage cost' + item.manaCost)">{{ item.inHand ? item.summonCost : item.manaCost }}</div>
                   <div class="attack" :title="'Attack: '+item.attack">{{ item.attack }}</div> 
                   <div class="manaPerRound" :title="'Mana per turn: '+item.manaPerTurn">{{ item.manaPerTurn }}</div> 
                 </div> 
               </div>
               <br v-if="item.name !== null">`,
   'props': {
              'item': Card,
              'turnManager': TurnManager, 
              'colors': Object,
              'gameOver': Boolean
            }
});

src/Dark.js

class EnemyUnit extends Card {
   constructor(summonCost, maxHealth, attack, nameColor, name, manaCost, hand=enemyHand, deck=enemyDeck, manaPerTurn="N/A", manaManager=enemyManaManager, inHand=false) {
     super(maxHealth, attack, nameColor, manaCost, name, inHand, hand, deck, manaPerTurn);
     this.summonCost = summonCost || 0
  };
};
class Demon extends EnemyUnit {
  constructor(hand, deck) {
    super(10, 100, 10, "#DFB720", "Demon", 7, hand, deck);
  };
};
class HellHound extends EnemyUnit {
  constructor(hand, deck) {
    super(5, 60, 5, "#C0C0C0", "Hell Hound", 4, hand, deck);
  };
};
class Cuthulu extends Primal {
  constructor() {
    super(enemyHand, enemyDeck, "Cuthulu");
  };
};
class ThornKnight extends EnemyUnit {
  constructor(hand, deck) {
    super(5, 20, 5, "#DF5F30", "Thorn Knight", 3, hand, deck);
  };
};

src/expandWeights.js

function expandWeights(weights) {
  let result = [];
  weights.forEach( (item) => {
    let weightedItem = item[0];
    for(var i = 0; i < item[1]; i++) {
      result.push(weightedItem);
    };
  });
  return result;
};

src/Land.js

class Stone extends Land {
  constructor(hand, deck, name) {
    super(1, name, '#B0C4DE', deck.isHand, hand, deck);
  };
};
class Mountain extends Stone {
  constructor(hand=playerHand, deck=playerDeck) {
    super(hand, deck, "Mountain");
  };
};
class Ravine extends Stone {
  constructor(hand=enemyHand, deck=enemyDeck) {
    super(hand, deck, "Ravine");
  };
};

src/Crippler.js

class Crippler extends Card {
  constructor(hand, deck, name, effects, maxHealth, summonCost) {
    super(maxHealth, "N/A", "#C0C0C0", 9, name, true, hand, deck, "N/A");
    this.summonCost = summonCost || 0;
    this.effects = effects;
  };
};
class DarkAgent extends Crippler {
  constructor(hand=enemyHand, deck=enemyDeck) {
    super(hand, deck, "Dark Agent", {
      manaCost: new Effect(3, 3, "manaCost", 1),
      health: new Effect(-10, 3, "health", -5),
      attack: new Effect(-3, 3, "attack", -5)
    }, 20, 10);
  };
};
class Lamp extends Crippler {
  constructor(hand=enemyHand, deck=enemyDeck) {
    super(hand, deck, "Lamp", {
      manaCost: new Effect(3, 3, "manaCost", 1),
      health: new Effect(-10, 3, "health", -5),
      attack: new Effect(-3, 3, "attack", -5)
    }, 20, 10);
  };
};

src/Light.js

class PlayerUnit extends Card {
  constructor(summonCost, maxHealth, attack, nameColor, name, manaCost, hand=playerHand, deck=playerDeck, manaPerTurn="N/A",manaManager=playerManaManager, inHand=false) {
    super(maxHealth, attack, nameColor, manaCost, name, inHand, hand, deck, manaPerTurn);
    this.summonCost = summonCost || 0;
  };
};
class EternalFlame extends Primal {
  constructor() {
    super(playerHand, playerDeck, "Eternal Flame");
  };
};
class Angel extends PlayerUnit {
  constructor(hand, deck) {
    super(10, 100, 10, "#DFB720", "Angel", 7, hand, deck);
  };
};
class Paladin extends PlayerUnit {
  constructor(hand, deck) {
    super(5, 60, 5, "#C0C0C0", "Paladin", 4, hand, deck);
  };
};
class Priest extends PlayerUnit {
  constructor(hand, deck) {
    super(5, 20, 5, "#DF5F30", "Priest", 3, hand, deck);
  };
};

game.html

<html>
  <head>
    <script src="./lib/vue.js"></script>
    <title>Angels & Demons</title>
    <link rel="stylesheet" href="./data/styles.css">
    <script src="./src/BaseClasses.js"></script>
    <script src="./src/components.js"></script>
    <script src="./src/Dark.js"></script>
    <script src="./src/Light.js"></script>
    <script src="./src/Land.js" ></script>
    <script src="./src/Crippler.js"></script>
    <script src="./data/DarkHand.js"></script>
    <script src="./data/LightHand.js"></script>
    <script src="./src/expandWeights.js"></script>
  </head>
  <div id="sandbox"></div>
  <body>
    <div id="game" style="display:block">
          <div style="background-color:#FFFFFF; width:100px;">
            <div class="manaBar" :style="{height:'20px',width: playerMana.manaBarWidth}"> {{ playerMana.mana }} </div>
          </div>
          <div style="float:right; background-color:#FFFFFF; width:100px;">
            <div class="manaBar" :style="{'float':'right', 'width': enemyMana.manaBarWidth}"> {{ enemyMana.mana }} </div>
          </div>
          <br><br>
          <div id="playerHand">
             <card v-for="item in playerHand" :key="item.ID" :item="item" :turn-manager="turnManager" :colors="colors" :game-over="gameOver"></card>
          </div>
          <div id="playerUnits">
                <card v-for="item in playerCards" :key="item.ID" :item="item" :turn-manager="turnManager" :colors="colors" :game-over="gameOver"></card>
          </div>
          <div id="turnStats">
            <h3 id="turn"> Turn: {{ turnManager.turnNumber % 2 ? "Demons" : "Angels" }} </h3>
            <button :onclick=" gameOver ? 'void(0)' : 'nextTurn()'">End turn.</button>
          </div>
          <div id="enemyUnits"  style="float:right;">
            <card v-for="item in enemyCards" :key="item.ID" :item="item" :turn-manager="turnManager" :colors="colors" :game-over="gameOver"></card>
          </div>
          <div id="enemyHand" style="float:right;">
            <card v-for="item in enemyHand" :key="item.ID" :item="item" :turn-manager="turnManager" :colors="colors" :game-over="gameOver"></card>
          </div>
      </table>
    </div>
    <script>
      const turnManager = new TurnManager;
      var gameOver = false;
      function getRandomItem(array){
        return array[Math.floor(Math.random()*array.length)]
      };

      function nextTurn(){
        turnManager.nextTurn();
        if( turnManager.turnNumber % 2 === 0 ) {
          playerManaManager.manaGain = playerManaManager.manaPerTurn;
          try{playerHand.addCardFromWeights()}catch(e){};
          playerDeck.updateEffects()
        } else if ( turnManager.turnNumber % 2 === 1 ) {
          enemyManaManager.manaGain = enemyManaManager.manaPerTurn;
          try{enemyHand.addCardFromWeights()}catch(e){};
        };
        playerDeck.ArrayOfCardIDs.forEach( (cardID) => {
          Object.assign(playerDeck.cards[cardID], {
            used: false,
            selected: false,
            locked: false
          });
        });
        playerDeck.selectedCardID = -1;
        enemyDeck.selectedCardID = -1;
        enemyDeck.ArrayOfCardIDs.forEach( (cardID) => {
          Object.assign(enemyDeck.cards[cardID], {
            used: false,
            selected: false,
            locked: false
          });
        });
      };
      const DarkerColors = {
        "#DFB720": "#BF5700",
        "#B0C4DE": "#90A4CE",
        "#C0C0C0": "#A0A0A0",
        "#DD00DD": "#BB00BB",
        "#DF5F30": "#BF3F10"
      },
           LighterColors = {
        "#DFB720": "#FFD940",
        "#B0C4DE": "#D0E4FE",
        "#C0C0C0": "#E0E0E0",
        "#DD00DD": "#FD00FD ",
        "#DF5F30": "#FF7F50"
      }
      var playerDeck = new Deck(false, null, playerWeights),
          enemyDeck = new Deck(false, null, enemyWeights),
          enemyHand = new Deck(true, new ManaManager(enemyDeck), enemyWeights),
          playerHand = new Deck(true, new ManaManager(playerDeck), playerWeights),
          playerManaManager = playerHand.manaManager,
          enemyManaManager = enemyHand.manaManager;
      Object.assign(enemyDeck, {
        manaManager: enemyManaManager,
        enemyDeck: playerDeck
      });
      playerDeck.manaManager = playerManaManager;
      playerDeck.enemyDeck = enemyDeck;
      enemyDeck.Lockdown()

      new Vue({
        el: "#game",
        data: {
          playerCards: playerDeck.cards,
          enemyCards: enemyDeck.cards,
          playerHand: playerHand.cards,
          enemyHand: enemyHand.cards,

          playerMana: playerManaManager,
          enemyMana: enemyManaManager,
          colors: {
            DarkerColors: DarkerColors,
            LighterColors: LighterColors
          },

          turnManager: turnManager,
          gameOver: gameOver
        }
      });
      playerDeck.sealCards();
      playerHand.sealCards();
      enemyDeck.sealCards();
      enemyHand.sealCards();
      nextTurn();
    </script>
  </body>
</html>
\$\endgroup\$
  • \$\begingroup\$ If you are planning on reviewing a file, please leave a comment telling me which file. I will try not to change the specified file. \$\endgroup\$ – FreezePhoenix Jun 13 '18 at 14:48
3
+50
\$\begingroup\$

Disclaimer:
I will slip into C# syntax from time to time, as I have little direct typescript experience. I'm focusing on the principle more than the actual syntax.


Class abuse

This is something that you've peppered through your codebase:

class Demon extends EnemyUnit {
  constructor(hand, deck) {
    super(10, 100, 10, "#DFB720", "Demon", 7, hand, deck);
  };
};
class HellHound extends EnemyUnit {
  constructor(hand, deck) {
    super(5, 60, 5, "#C0C0C0", "Hell Hound", 4, hand, deck);
  };
};

These inherited classes are not actually different from their EnemyUnit base class. They should not be classes in and of themselves.

This is the equivalent of doing things like:

class Number {
    constructor(value) {
        this.Value = value;
    }
}

class Five extends Number {
    constructor() { 
        super(5)
    }
}

class Twenty extends Number {
    constructor() {
        super(20)
    }
}

var myAge = new Twenty();
var fingersOnMyLeftHand = new Five();

My example is more blatant, but it's the same principle.

You shouldn't create new classes when you want to change values. The point of having a class is to have a single reusable type that can contain different values.

Just like my example should be rewritten to:

myAge = Number(20);
fingersOnMyLeftHand = Number(5);

Your code should be rewritten to:

function CreateDemon(hand, deck) {
    return new EnemyUnit(10, 100, 10, "#DFB720", "Demon", 7, hand, deck);
}

function CreateHellHound(hand, deck) {
    return new EnemyUnit(5, 60, 5, "#C0C0C0", "Hell Hound", 4, hand, deck);
}

Don't use inheritance as a way to create some preset values. That is not what inheritance is for.

Only inherit if you wish to create a new unit whose behavior is different to that of the existing EnemyUnit type. (e.g. attack is always equal to health, or a "hero" unit type).

For game design specifically, you'll see that most games tend to not inherit for different unit types, but rather to have a single unit type with omittable properties.
For example, if you have both physical armor and magical armor, you don't need to create separate classes for these. Just give EnemyUnit both PhysicalArmor and MagicalArmor properties, and simply set them to 0 if the current enemy doesn't have that particular type of armor.


A very minor comment, it's Cthulhu, not Cuthulu. It is phonetically pronounced "Cuthulu" though.


class TurnManager {
  constructor() {
    this.turnNumber = -1;
  };
  nextTurn() {
    this.turnNumber++;
  };
};

This isn't necessarily wrong, but why is turnNumber set to -1?

I suspect that you're thinking in a zero-indexed environment, and that you're relying on calling nextTurn() at the start of the game. But I think there are more intuitive approaches here:

  • Card games start at turn 1, not 0. Don't mix code-behind indexes and user interface numbers.
    • For example, I would consider turnList[oneIndexedTurnNumber-1] more readable than doing the inverse alert("Starting turn " + (zeroIndexedTurnNumber+1))". From a technical perspective, either can be correct, but the first snippet's context (using it as an array index) immediately explains why you're doing -1. The second snippet doesn't immediately explain why you're doing +1 (if you assume that the variable name does not actually contain "zero-indexed", of course).
  • To that end, I would suggest a strict naming convention: Numbers are 1-indexed, indexes are 0-indexed. Which means you need to either rename your field to turnIndex, or rework it to start from 1.
  • I wouldn't call nextTurn() at the start of a game (and at the start of a turn), but rather after a turn has completed. This makes more sense semantically (and logically). As a simple example: a for loop doesn't let its index start at -1 and then autoincrements it before iterating the first time. It's counterintuitive behavior.

\$\endgroup\$
  • \$\begingroup\$ nextTurn() is called at the end of each turn. However, I do think that I should start at turn 1. Also, calling nextTurn() at the start of the game was how I got it to start at turn 1. \$\endgroup\$ – FreezePhoenix Jun 29 '18 at 17:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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