broderclocks.js

/**
 * BORDERCLOCKS: Manages the ClockInferenceEngine and ClockUtil classes and communicates with the keyboard application. Handles switch-press events and makes selection decisions based on the clock probabilities.
 * @module broderclocks
 */

import * as cie from './clock_inference_engine.js';
import * as config from './config.js';

/**
 * Manages the ClockInferenceEngine and ClockUtil classes and communicates with the keyboard application. Handles switch-press events and makes selection decisions based on the clock probabilities.
 * @param {Keyboard} parent - the parent class, usually an instance of the Keyboard class
 * @property {ClockInference} clock_inf - The instance of the `ClockInference` class.
 * @property {Array<number>} abs_click_times - An array of the 'absolute' click times from the user leading up to the next selection.
 * Absolute click times are UNIX timestamps (ms since Epoch). `abs_click_times` are added as the user clicks.
 * @property {Array<number>} rel_click_times - An array of the 'relative' click times from the user leading up to the next selection.
 * Relative click times are the time in ms that the user clicked relative to when the clock they selected was at noon.
 * `rel_click_times` are added in `ClockInferenceEngine.learn_scores()` following `config.learn_delay` rounds after a selection is made.
 * @property {number} latest_time - The last time that the screen was updated as a UNIX timestamps (ms since Epoch). Updated in `ClockUtil.increment()`.
 * Used to account for the framerate when the user clicks.
 */
export class BroderClocks {

    constructor(parent) {
        this.parent = parent;
        this.parent.bc_init = true;
        this.clock_inf = new cie.ClockInference(this.parent, this, this.parent.prev_data);
        this.is_undo = false;
        this.is_equalize = false;

        this.latest_time = Date.now() / 1000;

        this.abs_click_times = [];
        this.rel_click_times = [];

        this.time_rotate = this.parent.time_rotate;
        this.clock_inf.clock_util.change_period(this.time_rotate);
    }

    /**
     * triggered by a switch-press event in the main keyboard class. Updates the clock posteriors and the histogram
     * given the information in the new click.
     * @param {float} time_in - The epoch-timestamp in ms that the user clicked their switch.
     */
    select(time_in) {

        this.clock_inf.update_scores(time_in - this.latest_time);
        if (config.is_learning) {
            this.clock_inf.update_history(time_in - this.latest_time);
        }

        // store the absolute click time
        this.abs_click_times.push(time_in);

        if (this.clock_inf.is_winner() && !this.parent.in_tutorial) {
            this.clock_inf.win_history[0] = this.clock_inf.sorted_inds[0];

            this.clock_inf.entropy.update_bits();
            this.parent.make_choice(this.clock_inf.sorted_inds[0]);

        } else {
            this.init_round(false, false, []);
        }
    }

    /**
     * Continues the select process after the word cache promise has loaded
     * @param results {Object} - Object with 5 parameters specifying the setup for the new round:
     * @param {Array<number>} results.words_on - the clocks turned on given the new word predictions
     * @param {Array<number>} results.words_off - the clocks turned off given the new word predictions
     * @param {Array<number>} results.word_score_prior - the prior distribution for the clocks_on given the new lm results
     * @param {Boolean} results.is_undo - whether the undo clock was selected in the last selection round
     * @param {Boolean} results.is_equalize - ? holdover from an obsolete corrective character
     * @param {Boolean} results.skip_hist - ? need to figure this out
     */
    continue_select(results) {
        this.clock_inf.clocks_on = results.words_on;
        this.clock_inf.clocks_off = results.words_off;
        var clock_score_prior = results.word_score_prior;
        this.is_undo = results.is_undo;
        this.is_equalize = results.is_equalize;
        var skip_hist = results.skip_hist;

        if (skip_hist) {
            this.init_round(true, true, clock_score_prior);
        } else {
            if (!this.parent.in_tutorial) {
                this.clock_inf.learn_scores(this.is_undo);
            }

            this.init_round(true, false, clock_score_prior);
        }
    }

    /**
     * re-initializes variables at the beginning of a new selection round
     */
    init_bits() {
        this.bits_per_select = Math.log(this.clock_inf.clocks_on.length) / Math.log(2);
        this.num_bits = 0;
    }

    /**
     * re-initializes variables at the beginning of a new selection round.
     * @param {Array<number>} clock_score_prior - the prior probabilities from the language model of the clocks currently on.
     */
    init_follow_up(clock_score_prior) {
        this.init_round(false, false, clock_score_prior);

        this.clock_inf.clock_history = [[]];
        this.clock_inf.win_history = [-1];

        this.init_bits();
    }

    /**
     * Sets up the inference aspects for a new round following a switch event
     * @param {Boolean} is_win - Whether a clock is selected after the switch event.
     * @param {Boolean} is_start - Whether this is the first round after a switch event.
     * @param {Array<number>} clock_score_prior - The prior probabilities over the active clocks before the switch event.
     */
    init_round(is_win, is_start, clock_score_prior) {
        this.clock_inf.clock_util.init_round(this.clock_inf.clocks_li);
        this.clock_inf.clock_util.init_round(this.clock_inf.clocks_on);
        var clock;
        var clock_ind;
        var top_score;

        if (is_win || is_start) {
            var count = 0;
            if (this.is_undo && !this.is_equalize) {
                for (clock_ind in this.clock_inf.clocks_on) {
                    clock = this.clock_inf.clocks_on[clock_ind];
                    this.clock_inf.cscores[clock] = 0;
                    count += 1;
                }
                top_score = 0;
            } else {
                for (clock_ind in this.clock_inf.clocks_on) {
                    clock = this.clock_inf.clocks_on[clock_ind];
                    this.clock_inf.cscores[clock] = clock_score_prior[count];
                    count += 1;
                }
                top_score = 0;
            }
        }

        this.clock_inf.update_sorted_inds();

        this.clock_inf.clock_util.update_curhours(this.clock_inf.sorted_inds);

        this.clock_inf.handicap_cscores(is_win, is_start);
        top_score = this.clock_inf.cscores[this.clock_inf.sorted_inds[0]];

        var bound_score;
        if (this.clock_inf.clock_history[0].length === 0) {
            bound_score = top_score - config.max_init_diff;
        } else {
            bound_score = top_score - this.parent.win_diffs[this.clock_inf.sorted_inds[0]];
        }

        for (var i in this.clock_inf.clocks_on) {
            clock_ind = this.clock_inf.clocks_on[i];
            clock = this.parent.clockgrid.clocks[clock_ind];

            if (this.clock_inf.cscores[clock_ind] > bound_score) {
                clock.highlighted = true;
                clock.draw_face();
            } else {
                clock.highlighted = false;
                clock.draw_face();
            }
        }
    }
}