7
\$\begingroup\$

As part of learning Vue and Vue Test Utils I develop some simple games. I made a simple version of craps. Because in many other games a die (or dice) is used, I decided to create a reusable component GameDie.

GameDie.vue

<template>
  <span class="dice">{{ dice.icon }}</span>
</template>

<script>
export default {
  name: 'GameDie',
  data() {
    return {
      dieFaces: [
        {
          value: undefined,
          icon: '🎲',
        },
        {
          value: 1,
          icon: '⚀',
        },
        {
          value: 2,
          icon: '⚁',
        },
        {
          value: 3,
          icon: '⚂',
        },
        {
          value: 4,
          icon: '⚃',
        },
        {
          value: 5,
          icon: '⚄',
        },
        {
          value: 6,
          icon: '⚅',
        },
      ],
      value: undefined,
    };
  },
  computed: {
    dice() {
      return this.dieFaces.filter((el) => el.value === this.value).shift();
    },
  },
  methods: {
    /**
     * Generate a random number between 1 and 6.
     * Assign it to the property value.
     */
    throwDice() {
      this.value = 1 + Math.floor(Math.random() * 6);
      this.$eventBus.$emit('transmit-dice', this.value);
    },
    /**
     * Reset the dice.
     */
    resetDice() {
      if (this.value) {
        this.value = undefined;
      }
    },
  },
  created() {
    this.$eventBus.$on('throw-dice', this.throwDice);
    this.$eventBus.$on('reset-dice', this.resetDice);
  },
};
</script>

<style scoped>
  .dice {
    font-size: 4em;
  }
</style>

I use emojis for the dice. When the die is thrown the value is calculated with Math.random. The component relies on a global event bus.

My main.js looks like this:

import Vue from 'vue';
import { BootstrapVue } from 'bootstrap-vue';
import App from './App.vue';

import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';

Vue.use(BootstrapVue);

Vue.config.productionTip = false;

Vue.prototype.$eventBus = new Vue();

new Vue({
  render: (h) => h(App),
}).$mount('#app');

The GameDie.vue component can now be used for different games, for craps or backgammon you need two dices:

<template>
  <div>
    <game-die></game-die>
    <game-die></game-die>
  </div>
</template>

For yamb you would need for example 5 dice, for Mensch ärgere Dich nicht just one.

Since I didn't have any other idea I used a global event bus to trigger the throwing and resetting the die. If we include GameDie.vue as a child component then the parent should actually communicate with it by passing props. But I was not sure how could I implement this.

The unit tests are here:

import { expect } from 'chai';
import sinon from 'sinon';
import mergeWith from 'lodash.mergewith';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vue from 'vue';
import GameDie from '@/components/GameDie.vue';

/* eslint-disable no-param-reassign */
const GlobalPlugins = {
  install(v) {
    v.prototype.$eventBus = new Vue();
  },
};
/* eslint-enable no-param-reassign */
const localVue = createLocalVue();
localVue.use(GlobalPlugins);

describe('GameDie.vue', () => {
  function createWrapper(overrides) {
    const options = {
      mocks: {
        $eventBus: {
          $on: sinon.spy(),
          $emit: sinon.spy(),
        },
      },
    };
    return shallowMount(GameDie, mergeWith(options, overrides));
  }

  it('should render the dice', () => {
    const wrapper = createWrapper();
    expect(wrapper.text()).to.equal('🎲');
  });

  it('should render a die face', () => {
    const data = () => ({
      value: 2,
    });
    const wrapper = createWrapper({ data });
    expect(wrapper.vm.value).to.equal(2);
    expect(wrapper.text()).to.equal('⚁');
  });

  it('should listen to custom events', () => {
    const wrapper = createWrapper();
    expect(wrapper.vm.$eventBus.$on.calledTwice).to.equal(true);
    expect(wrapper.vm.$eventBus.$on.calledWith('throw-dice')).to.equal(true);
    expect(wrapper.vm.$eventBus.$on.calledWith('reset-dice')).to.equal(true);
  });

  it('should listen to custom event throw-dice', () => {
    const throwDiceSpy = sinon.spy();
    const wrapper = shallowMount(GameDie, {
      localVue,
      methods: {
        throwDice: throwDiceSpy,
      },
    });
    wrapper.vm.$eventBus.$emit('throw-dice');
    expect(throwDiceSpy.called).to.equal(true);
  });

  it('should listen to custom event reset-dice', () => {
    const resetDiceSpy = sinon.spy();
    const wrapper = shallowMount(GameDie, {
      localVue,
      methods: {
        resetDice: resetDiceSpy,
      },
    });
    wrapper.vm.$eventBus.$emit('reset-dice');
    expect(resetDiceSpy.called).to.equal(true);
  });

  it('should emit a custom event transmit-dice', () => {
    const wrapper = createWrapper();
    wrapper.vm.throwDice();
    expect(wrapper.vm.$eventBus.$emit.called).to.equal(true);
  });
});

What can be improved here? Is there a better way using props? Or maybe Vue.js is not an adequate tool for programming games?

For the unit tests I used the input from a question I posted on StackOverflow. If the approach with the global event bus is good, how would you enhance the tests?

\$\endgroup\$
1
  • \$\begingroup\$ Oh there's definitely nothing wrong with using Vue.js for programming games. I've done so myself for a few games. \$\endgroup\$ Commented Feb 26, 2020 at 9:15

1 Answer 1

3
\$\begingroup\$

First of all, communicating through father-son relation component-wise should be with the event bus and props whereas it is recommended to send events from father to son with props.

I would use a prop to change the specific instance of the dice because you will be triggering a single event to a single die. If you would indeed want this to happen, I would use a single data property to bind all the dice from the father component.

With your implementation - all dice would reset/re-roll and you would get unknown behavior in the future if you would want to re-use your die. This would be a bad practice and you want to implement components as a single monad with a designed functionality without side effects. (more on monads here)

Secondly I would stick with 'die' or 'dice' for the sake of consistency and readability.

Thirdly, Vue has $emit as a native prototype so there's no need to configure it in your main.js you probably already know it but here's the resource. Also, a bit petty but alas, You're not using Bootstrap but you are importing it to your project.

PS

Another solution would be using Vuex for non father-son relation, perhaps holding an array of dice and changing their state that would trigger a single event for the responsible die

Edit

Here's a working git example of using props with your code

\$\endgroup\$
2
  • \$\begingroup\$ Thank you very much for your answer. Unfortunately the GitHub repository seems to have just the boilerplate code for a Vue.JS app. Have you maybe forgotten to push the actual code? \$\endgroup\$
    – cezar
    Commented May 21, 2020 at 19:36
  • \$\begingroup\$ Yes you are correct, no idea how that happened :S Fixed that :) \$\endgroup\$ Commented May 22, 2020 at 13:44

Your Answer

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

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