core/finite-state-machine.js

/**
 * Creates finite state machines.
 * @template {string} TypeGeneric The generic type of the names of a state.
 *
 * @example
 *
 * const toggle = new FiniteStateMachine([
 *
 *     {
 *         $state: 'ON',
 *         $transitions: [{
 *
 *             $state: 'OFF',
 *             $condition: ({$timer}) => ($timer >= 1000)
 *         }]
 *     },
 *     {
 *         $state: 'OFF',
 *         $transitions: [{
 *
 *             $state: 'ON',
 *             $condition: ({$timer}) => ($timer >= 1000)
 *         }]
 *     }
 * ]);
 *
 * toggle.tick(timetick);
 */
class FiniteStateMachine {

    /**
     * @callback TypeStateHandlerEnter A state entering handler.
     * @param {Object} $parameters The given parameters.
     * @param {TypeGeneric} $parameters.$previous The previous state.
     * @returns {void}
     * @protected
     *
     * @memberof FiniteStateMachine
     */

    /**
     * @callback TypeStateHandlerLeave A state leaving handler.
     * @param {Object} $parameters The given parameters.
     * @param {number} $parameters.$timer The timer of the current state.
     * @param {TypeGeneric} $parameters.$next The next state.
     * @returns {void}
     * @protected
     *
     * @memberof FiniteStateMachine
     */

    /**
     * @callback TypeStateHandlerTick A state updating handler.
     * @param {Object} $parameters The given parameters.
     * @param {number} $parameters.$timetick The tick duration (in ms).
     * @param {number} $parameters.$timer The timer of the current state.
     * @returns {void}
     * @protected
     *
     * @memberof FiniteStateMachine
     */

    /**
     * @callback TypeStateTransitionCondition A state transition condition.
     * @param {Object} $parameters The given parameters.
     * @param {TypeGeneric} $parameters.$previous The previous state.
     * @param {number} $parameters.$timer The timer of the current state.
     * @returns {boolean}
     * @protected
     *
     * @memberof FiniteStateMachine
     */

    /**
     * @typedef {Object} TypeStateTransition A transition to a state.
     * @property {TypeStateTransitionCondition} TypeStateTransition.$condition The condition to transition to given state.
     * @property {TypeGeneric} TypeStateTransition.$state The given state to transition to.
     * @protected
     *
     * @memberof FiniteStateMachine
     */

    /**
     * @typedef {Object} TypeState A state.
     * @property {TypeGeneric} TypeState.$state The name of the state.
     * @property {TypeStateHandlerEnter} [TypeState.$onEnter] The handler to execute when entering the state.
     * @property {TypeStateHandlerLeave} [TypeState.$onLeave] The handler to execute when leaving the state.
     * @property {TypeStateHandlerTick} [TypeState.$onTick] The handler to execute when updating the state.
     * @property {Array<TypeStateTransition>} [TypeState.$transitions] The transitions to given states.
     * @protected
     *
     * @memberof FiniteStateMachine
     */

    /**
     * Stores the initiated status.
     * @type {boolean}
     * @private
     */
    $initiated;

    /**
     * Stores the previous state.
     * @type {TypeState}
     * @private
     */
    $previous;

    /**
     * Stores the current state.
     * @type {TypeState}
     * @private
     */
    $state;

    /**
     * Stores the states.
     * @type {Map<TypeGeneric, TypeState>}
     * @private
     */
    $states;

    /**
     * Stores the timer of the current state.
     * @type {number}
     * @private
     */
    $timer;

    /**
     * Gets the name of the current state.
     * @type {TypeGeneric}
     * @public
     */
    get state() {

        return this.$state.$state;
    }

    /**
     * Gets the initiated status.
     * @type {boolean}
     * @public
     */
    get initiated() {

        return this.$initiated;
    }

    /**
     * Creates a new finite state machine.
     * @param {Array<TypeState>} $data The representation of the finite state machine.
     */
    constructor($data) {

        this.$initiated = false;
        this.$states = new Map();
        this.$timer = 0;

        $data.forEach(($state) => {

            this.$states.set($state.$state, $state);
        });
    }

    /**
     * Initiates the finite state machine.
     * @param {TypeGeneric} $state The name of the state to initiate.
     * @public
     */
    initiate($state) {

        if (this.$initiated === true) {

            return;
        }

        this.$previous = this.$state
        this.$state = this.$states.get($state);

        if (typeof this.$state.$onEnter === 'function') {

            this.$state.$onEnter({$previous: undefined});
        }

        this.$initiated = true;
    }

    /**
     * Updates the finite state machine by one tick update.
     * @param {number} $timetick The tick duration (in ms).
     * @public
     */
    tick($timetick) {

        if (this.$initiated === false) {

            return;
        }

        this.$timer += $timetick;

        if (typeof this.$state.$onTick === 'function') {

            this.$state.$onTick({$timetick: $timetick, $timer: this.$timer});
        }

        const transitions = this.$state.$transitions;

        if (Array.isArray(transitions) === false) {

            return;
        }

        for (let $transition of transitions) {

            let previous;

            if (typeof this.$previous !== 'undefined') {

                previous = this.$previous.$state;
            }

            const current = this.$state.$state;
            const next = $transition.$state;

            if ($transition.$condition({$previous: previous, $timer: this.$timer}) === true) {

                if (typeof this.$state.$onLeave === 'function') {

                    this.$state.$onLeave({$timer: this.$timer, $next: next});
                }

                this.$timer = 0;

                this.$previous = this.$state;
                this.$state = this.$states.get(next);

                if (typeof this.$state.$onEnter === 'function') {

                    this.$state.$onEnter({$previous: current});
                }

                this.tick(0);

                break;
            }
        }
    }
}

export {

    FiniteStateMachine
};

export default FiniteStateMachine;