uccser/cs-field-guide

View on GitHub
csfieldguide/static/interactives/parity/js/parity.js

Summary

Maintainability
D
2 days
Test Coverage
var Parity = {};

$(document).ready(function(){
  // Create the grid on load
  Parity.grid = $('#interactive-parity-grid');
  Parity.feedback = $('#interactive-parity-feedback');
  Parity.x_labels = document.getElementById('interactive-parity-grid-x-labels');
  Parity.y_labels = document.getElementById('interactive-parity-grid-y-labels');

  let searchParameters = new URL(window.location.href).searchParams;

  if (searchParameters.has('grid-size')) {
    let selectElement = document.getElementById('interactive-parity-grid-size');
    let value = searchParameters.get('grid-size');
    if (!isNaN(value)) {
      selectElement.value = value;
    }
  }
  if (searchParameters.has('show-grid-references')) {
    document.getElementById('grid-references-checkbox').checked = true;
    showGridReferences(true);
  }
  if (searchParameters.has('hide-text')) {
    document.getElementById('interactive-parity').classList.add('hide-text');
  }
  if (searchParameters.has('initial-bits')) {
    Parity.initial_bits = searchParameters.get('initial-bits');
  }
  Parity.hide_size_controls = searchParameters.has('hide-size-controls');

  if (searchParameters.get('mode') == 'sandbox') {
    Parity.mode = 'sandbox';
    Parity.current_mode = 'sandbox';
  } else if (searchParameters.get('mode') == 'set') {
    Parity.mode = 'set';
    Parity.current_mode = 'set';
  } else if (searchParameters.get('mode') == 'detect') {
    Parity.mode = 'detect';
    Parity.current_mode = 'detect';
  } else {
    Parity.mode = 'trick';
    Parity.current_mode = 'set';
  }

  setupGrid();
  setupGridLabels();
  setupMode();

  // On 'Check parity' button click
  $('#interactive-parity-check').on('click', function(){
    var parity_status = checkParity();
    // Display status
    if (parity_status) {
      Parity.feedback.removeClass('error');
      Parity.feedback.text(gettext("Correct: Every row and column has even parity (of black squares)"));
    } else {
      Parity.feedback.addClass('error');
      Parity.feedback.text(gettext("Incorrect: Not every row and column has even parity (of black squares)"));
    }
  });

  // On 'Add random data' button click
  $('#interactive-parity-random-data').on('click', function(){
    clearGrid();
    setRandomBits();
    updateGrid();
  });

  // On 'Clear all' button click
  $('#interactive-parity-clear-all').on('click', function(){
    clearGrid();
  });

  // On toggle of grid references checkbox
  $('#grid-references-checkbox').on('change', function(){
    showGridReferences(this.checked);
  });

  // On 'Reset' button click
  $('#interactive-parity-reset').on('click', function(){
    // If set stage in set or trick mode
    if (Parity.current_mode == 'set') {
      clearGrid();
      setRandomBits();
      updateGrid();
    // Else if detect stage in trick mode
    } else if (Parity.mode == 'trick') {
      Parity.valid_parity_bits = undefined;
      Parity.current_mode = 'set';
      clearGrid();
      setupMode()
    // Else if detect stage in detect mode
    } else {
      clearGrid();
      setupMode();
    }
    // Remove green success highlight around bit if correct bit found
    $('#interactive-parity-grid .parity-card').removeClass('correct-bit');
    Parity.feedback.removeClass('error');
    Parity.feedback.text("");
    updateCircler('x', -1);
    updateCircler('y', -1);
  });

  // When the user click on the 'Flip a bit' button
  $('#interactive-parity-flip-bit').on('click', function(){
    updateCircler('x', -1);
    updateCircler('y', -1);
    $('.interactive-parity-size-controls').hide();
    $('.interactive-parity-trick-controls').hide();
    $('.interactive-parity-reset-controls').hide();
    $('#interactive-parity-grid-confusation').css( "display", "flex");
    timer(50, 2000, randomlyToggleBits, setupParityTrick);
  });

  // Toggle parity card when user clicks on card
  $('#interactive-parity-grid').on('click', '.parity-card', function(event) {
    $bit = $(this);
    if (Parity.valid_parity_bits == false) {
      Parity.feedback.addClass('error');
      Parity.feedback.text(gettext("Your parity bits weren't set correctly, try starting again."));
    } else if (Parity.current_mode == 'detect' && Parity.flipping == 'all') {
      if ($bit.data("row") == Parity.flipped_row && $bit.data("col") == Parity.flipped_col) {
        $bit.addClass("correct-bit");
        Parity.feedback.removeClass('error');
        Parity.feedback.text(gettext("Correct! You spotted the flipped bit!"));
        Parity.flipping = 'none';
      } else {
        Parity.feedback.addClass('error');
        Parity.feedback.text(gettext("That's not the flipped bit!"));
      }
    } else if (Parity.flipping == 'all' || (Parity.flipping == 'parity' && $bit.hasClass('parity-bit'))) {
      // We toggle bit manually rather than call updateGrid() to stay O(1)
      $bit.toggleClass('black');
      Parity.grid_values[$bit.data("row")][$bit.data("col")] = !$bit.hasClass("black");
    }
  });

  // Change grid size on value change
  $('#interactive-parity-grid-size').on('change', function(){
      setupGrid();
      setupGridLabels();
      setupMode();
  });
});


// This function is trigger when the confusation stage is over.
function setupParityTrick() {
  // Check if parity was incorrect before
  Parity.valid_parity_bits = checkParity();
  // Update interface for current mode
  Parity.current_mode = 'detect';
  setupMode();
  // Flips a bit in memory
  flipBit();
  // Displays new grid
  updateGrid();
  // Hides the confusation overlay
  $('#interactive-parity-grid-confusation').hide();
  // Allow flipping of bit
  Parity.flipping = 'all';
};


// Set interface for current mode
function setupMode() {
  var header = $('#interactive-parity-mode');

  // Hide all controls
  $('.interactive-parity-controls').hide()

  if (Parity.current_mode == 'sandbox') {
     header.text("Sandbox Mode");
     $('.interactive-parity-sandbox-controls').show();
     if (!Parity.hide_size_controls) {
      $('.interactive-parity-size-controls').show();
     }
     $('.interactive-parity-check-controls').show();
     Parity.flipping = 'all';
     setRandomBits();
     updateGrid();
  } else if (Parity.current_mode == 'set') {
    header.text(gettext("Setting Parity"));
    Parity.flipping = 'parity';
    if (!Parity.hide_size_controls) {
      $('.interactive-parity-size-controls').show();
    }
    if (Parity.mode == 'trick') {
      $('.interactive-parity-trick-controls').show();
    } else {
      $('.interactive-parity-check-controls').show();
    }
    $('.interactive-parity-reset-controls').show();
    setRandomBits();
    updateGrid();
  } else if (Parity.current_mode == 'detect') {
    header.text(gettext("Detect the Error"));
    Parity.flipping = 'all';
    $('.interactive-parity-detect-controls').show();
    $('.interactive-parity-reset-controls').show();
    // If detect only mode (not trick mode)
    if (Parity.mode == 'detect') {
      if (!Parity.hide_size_controls) {
        $('.interactive-parity-size-controls').show();
      }
      setRandomBits();
      setParityBits();
      flipBit();
      updateGrid();
    }
  }
};


// Calls a function at each interval (ms) for a certain length (ms)
function timer(interval, length, callback, finished_callback) {
  var start = Date.now();
  var intervalID;
  function handler() {
    if (Date.now() - start > length) {
      clearInterval(intervalID);
      finished_callback();
    }
    else if (callback !== undefined) {
      callback();
    }
  };
  intervalID = setInterval(handler, interval);
};


// This function flips one bit and does not update the grid
function flipBit() {
  Parity.flipped_row = Math.floor(Math.random() * Parity.grid_size);
  Parity.flipped_col = Math.floor(Math.random() * Parity.grid_size);
  Parity.grid_values[Parity.flipped_row][Parity.flipped_col] = !Parity.grid_values[Parity.flipped_row][Parity.flipped_col];
};


// Set random bit values (except for parity row)
function setRandomBits() {
  for (var row = 0; row < Parity.grid_values.length - 1; row++) {
    for (var col = 0; col < Parity.grid_values.length - 1; col++) {
      if (Parity.initial_bits) {
        let char_position = row * (Parity.grid_values.length - 1) + col;
        let initial_bit = Parity.initial_bits.charAt(char_position);
        if (initial_bit == 'W') {
          Parity.grid_values[row][col] = true;
        } else {
          Parity.grid_values[row][col] = false;
        }
      } else {
        if (Math.random() >= 0.5) {
          Parity.grid_values[row][col] = true;
        } else {
          Parity.grid_values[row][col] = false;
        }
      }
    }
  }
};


// Set random data values (except parity bits)
function updateGrid() {
  Parity.grid.children(':not(.circler)').each(function( row_index, row ) {
    $(row).children().each(function( col_index, col ) {
      $col = $(col);
      if (Parity.grid_values[row_index][col_index]) {
        $col.removeClass("black");
      } else {
        $col.addClass("black");
      }
    });
  });
};


// Clear all bits
function clearGrid() {
  for (var row = 0; row < Parity.grid_values.length; row++) {
    for (var col = 0; col < Parity.grid_values.length; col++) {
      Parity.grid_values[row][col] = true;
    }
  }
  updateGrid();
};


// Random change bits during confusation
function randomlyToggleBits() {
  Parity.grid.children(':not(.circler)').each(function( row_index, row ) {
    $(row).children().each(function( col_index, col ) {
      if (Math.random() >= 0.5) {
        if ($(col).hasClass("black")) {
          $(col).removeClass("black");
        } else {
          $(col).addClass("black");
        }
      }
    });
  });
};


// Set grid size and data values
function setupGrid(){
  // Get grid size and set it
  Parity.grid_size = parseInt($('#interactive-parity-grid-size').val());

  // Create 2D array of bit values - true is on (white) while false is off (black)
  Parity.grid_values = new Array(Parity.grid_size);
  for (var row = 0; row < Parity.grid_values.length; row++) {
    Parity.grid_values[row] = new Array(Parity.grid_size).fill(true);
  }

  // Clear feedback
  $('#interactive-parity-feedback').text('');

  if (Parity.grid_size > 21 || Parity.grid_size < 1 || isNaN(Parity.grid_size)) {
    // Error message
    alert(gettext("Please enter a value between 1 and 20"));
    // Reset grid
    $('#interactive-parity-grid-size').val(6);
  } else {
    Parity.grid.empty();

    // Add circlers
    let rowCircler = document.createElement('div');
    rowCircler.id = 'interactive-parity-grid-row-circler';
    rowCircler.classList.add('circler');
    Parity.grid.append(rowCircler);
    Parity.rowCircler = rowCircler;
    Parity.rowCirclerIndex = -1;
    let columnCircler = document.createElement('div');
    columnCircler.id = 'interactive-parity-grid-column-circler';
    columnCircler.classList.add('circler');
    Parity.grid.append(columnCircler);
    Parity.columnCircler = columnCircler;
    Parity.columnCirclerIndex = -1;

    Parity.x_labels.innerHTML = '';
    Parity.y_labels.innerHTML = '';
    for(row = 0; row < Parity.grid_size; row++) {
        var $gridRow = $('<div class="flex-container">');
        Parity.grid.append($gridRow);
        for(col = 0; col < Parity.grid_size; col++) {
            var $bit = $('<div class="flex-item parity-card"></div>');
            $gridRow.append($bit);
            // Storing the column and row positions on a bit
            // allows updating grid_values in O(1)
            $bit.data("col", col);
            $bit.data("row", row);
            if (row == Parity.grid_size - 1 || col == Parity.grid_size - 1) {
              $bit.addClass("parity-bit");
            }
        }
        Parity.grid.append('</div>');
    }
  }
};


// Create grid reference labels
function setupGridLabels(){
    for (let i = 0; i < Parity.grid_size; i++) {
        var xLabel = document.createElement('div');
        xLabel.addEventListener('click', function () {
            updateCircler('y', i);
        });
        let xLabelTextElement = document.createElement('div');
        xLabelTextElement.classList.add('interactive-parity-grid-labels-text')
        let xLabelText = String.fromCharCode(65 + i);
        xLabelTextElement.appendChild(document.createTextNode(xLabelText));
        xLabel.appendChild(xLabelTextElement);
        Parity.x_labels.appendChild(xLabel);

        var yLabel = document.createElement('div');
        yLabel.addEventListener('click', function () {
            updateCircler('x', i);
        });
        let yLabelTextElement = document.createElement('div');
        yLabelTextElement.classList.add('interactive-parity-grid-labels-text')
        let yLabelText = String(i + 1);
        yLabelTextElement.appendChild(document.createTextNode(yLabelText));
        yLabel.appendChild(yLabelTextElement);
        Parity.y_labels.appendChild(yLabel);
    }
};


// Return boolean of parity status
function checkParity() {
  var parity_status = true;

  column_counts = new Array(Parity.grid_values.length).fill(0);
  for (var row = 0; row < Parity.grid_values.length; row++) {
    var black_bit_count = 0;
    for (var col = 0; col < Parity.grid_values.length; col++) {
      // If white (true) add to counts
      if (Parity.grid_values[row][col] == 0) {
        black_bit_count++;
        column_counts[col]++;
      }
    }
    if (black_bit_count % 2 == 1) {
      parity_status = false;
    }
  }

  // Check column counts
  for (var col = 0; col < column_counts.length; col++) {
    if (column_counts[col] % 2 == 1) {
      parity_status = false;
    }
  }

  return parity_status;
};


// Sets the last row and column parity bits correctly
function setParityBits() {

  column_counts = new Array(Parity.grid_values.length).fill(0);
  for (var row = 0; row < Parity.grid_values.length-1; row++) {
    var black_bit_count = 0;
    for (var col = 0; col < Parity.grid_values.length-1; col++) {
      // If black add to counts
      if (Parity.grid_values[row][col] == 0) {
        black_bit_count++;
        column_counts[col]++;
      }
    }
    // Last bit in row
    if (black_bit_count % 2 == 0) {
      Parity.grid_values[row][Parity.grid_values.length-1] = false;
    } else {
      column_counts[Parity.grid_values.length-1]++;
    }
  }
  // Last bit in column
  for (var col = 0; col < column_counts.length; col++) {
    if (column_counts[col] % 2 == 0) {
      Parity.grid_values[Parity.grid_values.length-1][col] = false;
    }
  }
};


function showGridReferences(show) {
    var gridReferenceContainers = document.getElementsByClassName("interactive-parity-grid-labels");
    for (var i = 0; i < gridReferenceContainers.length; i++) {
        gridReferenceContainers[i].classList.toggle(`text-visible`, show);
    }
};

function updateCircler(axis, index) {
    var circler, top, left, bottom, right;
    var gridUnitPercentage = 100 / Parity.grid_size;
    if (axis == 'x') {
        circler = Parity.rowCircler;
        if (index != Parity.rowCirclerIndex && index != -1) {
            left = -1;
            right = -1;
            top = gridUnitPercentage * index;
            bottom = gridUnitPercentage * (Parity.grid_size - index - 1);
            Parity.rowCirclerIndex = index;
        } else {
            Parity.rowCirclerIndex = -1;
        }
    } else {
        circler = Parity.columnCircler;
        if (index != Parity.columnCirclerIndex && index != -1) {
            left = gridUnitPercentage * index;
            right = gridUnitPercentage * (Parity.grid_size - index - 1);
            top = -1;
            bottom = -1;
            Parity.columnCirclerIndex = index;
        } else {
            Parity.columnCirclerIndex = -1;
        }
    }
    // Check a property has been set
    if (top || left) {
        circler.style.inset = `${top}% ${right}% ${bottom}% ${left}%`;
        circler.style.visibility = 'visible';
    } else {
        circler.removeAttribute('style');
    }
}