core/finitestatemachine.js

/**
 * Creates finite state machines.
 * @template {string} T 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)
 *         }]
 *     }
 * ]);
 */
class FiniteStateMachine {

    /**
     * @callback typestatehandlerenter A state entering handler.
     * @param {Object} $parameters The given parameters.
     * @param {T} $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 {T} $parameters.$next The next state.
     * @returns {void}
     * @protected
     *
     * @memberof FiniteStateMachine
     */

    /**
     * @callback typestatetransitioncondition A state transition condition.
     * @param {Object} $parameters The given parameters.
     * @param {T} $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 {T} typestatetransition.$state The given state to transition to.
     * @protected
     *
     * @memberof FiniteStateMachine
     */

    /**
     * @typedef {Object} typestate A state.
     * @property {T} 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 {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<T, typestate>}
     * @private
     */
    $states;

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

    /**
     * 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 {T} $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.
     * @param {number} $timetick The tick duration (in ms).
     * @public
     */
    update($timetick) {

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

            return;
        }

        this.$timer += $timetick;

        for (let $transition of this.$state.$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});
                }

                break;
            }
        }
    }
}

export {

    FiniteStateMachine
};

export default FiniteStateMachine;