uccser/cs-field-guide

View on GitHub
csfieldguide/static/interactives/training-ground/js/training-ground.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * trAIning ground
 * 
 * Creates and executes the game
 */

var TABLE = require('./html-table.js');
var IMG_GRID = require('./image-grid.js');
var AI = require('./ai.js');
var noUiSlider = require('nouislider');

var chanceOfPlayerStart;
var numSticks;
var remainingSticks;
var aiSensitivity;
var numSimulations;
var networkTable;
var sticksGrid;
var ai;
var gamesPlayed;
var aiWins;
var isSimulation;
var isStart;
var doCancelSims;
var practiceOpponent;
var quickSims;

var whoStartsRange;
var numSticksRange;
var sensitivityRange;
var numSimulationsRange;

const stickPath = base + 'interactives/training-ground/img/stick.png';
const PLAYERS = {
  NONE: 0,
  // These next three match their HTML radiobutton value counterparts
  INTELLIBOT: 1,
  RANDOBOT: 2,
  AI_PRACTICE: 3,
  //
  HUMAN: 4,
  AI: 5,
}

var TXT_LOSS = gettext("Nathaniel wins");
var TXT_WIN = gettext("You win!");
var TXT_TURN = gettext("Your turn.");
var TXT_WAIT = gettext("Nathaniel is thinking.");
var TXT_SIMULATING_SMART = gettext("Simulating games<br>(smart opponent)");
var TXT_SIMULATING_RANDOM = gettext("Simulating games<br>(random opponent)");
var TXT_SIMULATING_ITSELF = gettext("Simulating games<br>(against itself)");
var TXT_SIMULATED = gettext("Simulations finished");
var TXT_INITIAL = gettext("Set the initial parameters below, then hit start");
var TXT_STARTTURN = gettext("You start, how many sticks will you remove?");
var TXT_AI_ALWAYS = gettext("Always");
var TXT_PLAYER_ALWAYS = gettext("Never");

$(document).ready(function() {
  isSimulation = false;
  createSliders();
  reset();
  refresh();

  $('#button_start').on('click', run);
  $('#button_modal_start').on('click', run);
  $('#button_1').on('click', function() {
    applyMove(PLAYERS.HUMAN, 1);
  });
  $('#button_2').on('click', function() {
    applyMove(PLAYERS.HUMAN, 2);
  });
  $('#button_3').on('click', function() {
    applyMove(PLAYERS.HUMAN, 3);
  });
  $('#practice-opponent-select').on('change', updateBotDescription);
  $('#button_rematch').on('click', rematch);
  $('#button_simulate').on('click', function() {
    simulate(10);
  });
  $('#button_cancel').on('click', function() {
    doCancelSims = true;
  });
  $('#button_modal_cancel').on('click', function() {
    doCancelSims = true;
  });
  $('#button_quit').on('click', reset);
  $('#button_modal_quit').on('click', reset);
  
  numSticksRange.noUiSlider.on('update', refresh);
});

/**
 * Builds and executes the game.
 */
function run() {
  isStart = true;
  refresh();
  $('#game-parameters').addClass('no-input');
  hideStartButtons();
  $('#status-text').html("...");
  ai = new AI.AI(numSticks, aiSensitivity);
  ai.init();
  networkTable.populateTable(ai.map);
  simulate(numSimulations);
}

/**
 * Recalculates the number of sticks to display and other initial parameters
 */
function refresh() {
  getParameters();
  networkTable.createTable(numSticks);
  sticksGrid.createGrid(numSticks);
  remainingSticks = numSticks;
  displayBaseVariables();
}

/**
 * Returns the game to its initial 'page loaded' state
 */
function reset() {
  whoStartsRange.noUiSlider.reset();
  numSticksRange.noUiSlider.reset();
  sensitivityRange.noUiSlider.reset();
  numSimulationsRange.noUiSlider.reset();
  $('#practice-opponent-select input:radio:checked').prop('checked', false).parent().removeClass('active');
  $('#radio_smartbot').prop('checked', true).parent().addClass('active');
  updateBotDescription();
  $('#quick-simulations-select input:radio:checked').prop('checked', false).parent().removeClass('active');
  $('#radio_false').prop('checked', true).parent().addClass('active');
  $('#status-text').html(TXT_INITIAL);
  gamesPlayed = 0;
  aiWins = 0;
  networkTable = new TABLE.HtmlTable($('#data-table'));
  sticksGrid = new IMG_GRID.ImageGrid($('#sticks-area'), stickPath, 7);
  refresh();
  hideQuitButtons();
  hideEndButtons();
  hideChoiceButtons();
  showStartButtons();
  $('#splash-text').addClass('d-none');
  $('#game-parameters').removeClass('no-input');
}

/**
 * Starts a new match within the game
 */
function rematch() {
  ai.newGame();
  networkTable.uncolourCells();
  updateRemainingSticks(numSticks);
  displayBaseVariables();
  if (!isSimulation) {
    sticksGrid.createGrid(numSticks);
    $('#splash-text').addClass('d-none');
    hideEndButtons();
    showChoiceButtons();
  }
  if (Math.random() < chanceOfPlayerStart) {
    // (no need to be more precise)
    $('#status-text').html(TXT_STARTTURN);
    readyPlayerTurn();
  } else {
    takeAiTurn();
  }
}

/**
 * Simulates the given number of games between the AI and a preset practice bot.
 * Runs as quickly as possible, without displaying anything to the user 
 */
function runQuickSimulation(num) {
  var aiTurn;
  var choice;
  while (num > 0) {
    ai.newGame();
    choice = 0;
    aiTurn = !(Math.random() < chanceOfPlayerStart);
    while (ai.sticksLeft > 0) {
      if (aiTurn) {
        choice = ai.takeTurn();
        ai.sticksLeft -= choice;
        aiTurn = false;
      } else {
        if (practiceOpponent == PLAYERS.AI_PRACTICE) {
          choice = ai.takeTurn(true);
        } else {
          choice = ai.takeBotTurn((practiceOpponent == PLAYERS.INTELLIBOT) ? true : false);
        }
        ai.sticksLeft -= choice;
        aiTurn = true;
      }
    }
    ai.updateAI(aiTurn); // If it is currently the AI's turn then its opponent won
    if (!aiTurn) {
      aiWins++;
    }
    gamesPlayed++;
    num--;
  }
  networkTable.populateTable(ai.map);
  displayBaseVariables();
}

/**
 * Simulates the given number of games between the AI and a preset practice bot
 */
function simulate(num) {
  if (quickSims) {
    runQuickSimulation(num);
    endSimulation();
    return;
  }
  if (practiceOpponent == PLAYERS.INTELLIBOT) {
    $('#splash-text').html(TXT_SIMULATING_SMART);
  } else if (practiceOpponent == PLAYERS.RANDOBOT) {
    $('#splash-text').html(TXT_SIMULATING_RANDOM);
  } else {
    $('#splash-text').html(TXT_SIMULATING_ITSELF);
  }
  $('#splash-text').removeClass('d-none');
  doCancelSims = false;
  isSimulation = true;
  disableChoiceButtons();
  hideChoiceButtons();
  hideEndButtons();
  showCancelButtons();
  recursiveSim(num);
}

/**
 * Simulates games between the AI and a preset practice bot recursively until there
 * are none left to do or the user cancels the simulation
 */
function recursiveSim(num) {
  if (doCancelSims || num <= 0) {
    endSimulation();
  } else {
    setTimeout(function() {recursiveSim(num - 1)}, 100);
    rematch();
  }
}

/**
 * Runs post-simulation setup for the game. If this is the initial game-start simulations
 * a new match is started immediately
 */
function endSimulation() {
  hideCancelButtons();
  $('#status-text').html(TXT_SIMULATED);
  networkTable.uncolourCells();
  $('#splash-text').addClass('d-none');
  isSimulation = false;
  enableQuitButtons();
  if (isStart) {
    isStart = false;
    showQuitButtons();
    rematch();
  } else {
    showEndButtons();
  }
}

/**
 * Stores the initial game parameters chosen by the user in appropriate variables
 */
function getParameters() {
  var chanceOfAiStart = whoStartsRange.noUiSlider.get();
  if (chanceOfAiStart == TXT_AI_ALWAYS) {
    chanceOfPlayerStart = 0;
  } else if (chanceOfAiStart == TXT_PLAYER_ALWAYS) {
    chanceOfPlayerStart = 1;
  } else {
    chanceOfPlayerStart = 1 - (parseInt(chanceOfAiStart) / 100);
  }

  numSticks = numSticksRange.noUiSlider.get();
  aiSensitivity = sensitivityRange.noUiSlider.get();
  numSimulations = numSimulationsRange.noUiSlider.get();

  var opponent = $('#practice-opponent-select input:radio:checked').val();
  if (opponent == "smart") {
    practiceOpponent = PLAYERS.INTELLIBOT;
  } else if (opponent == "random") {
    practiceOpponent = PLAYERS.RANDOBOT;
  } else {
    practiceOpponent = PLAYERS.AI_PRACTICE;
  }
  quickSims = $('#quick-simulations-select input:radio:checked').val() == 'true';
}

/**
 * Reveals the description for the currently selected bot. Hides the others
 */
function updateBotDescription() {
  $('#smartbot_description').addClass('d-none');
  $('#randobot_description').addClass('d-none');
  $('#nathaniel_description').addClass('d-none');
  getParameters();
  if (practiceOpponent == PLAYERS.INTELLIBOT) {
    $('#smartbot_description').removeClass('d-none');
  } else if (practiceOpponent == PLAYERS.RANDOBOT) {
    $('#randobot_description').removeClass('d-none');
  } else {
    $('#nathaniel_description').removeClass('d-none');
  }
}

/**
 * Displays base game variables: number of sticks remaining, number of wins, and number of games played
 */
function displayBaseVariables() {
  $('#remaining-sticks').html(remainingSticks);
  $('#ai-wins').html(aiWins);
  $('#games-played').html(gamesPlayed);
}

/**
 * Creates the nouislider sliders for choosing initial parameters.
 * 'format:' sections from https://stackoverflow.com/a/38435763
 */
function createSliders() {
  whoStartsRange = document.getElementById('who-starts-select');
  noUiSlider.create(whoStartsRange, {
    range: {
      'min': 0,
      'max': 100
    },
    start: 100,
    step: 10,
    tooltips: true,
    format: {
      to: function(value) {
        if (value == 100) {
          return TXT_AI_ALWAYS;
        }
        if (value == 0) {
          return TXT_PLAYER_ALWAYS;
        }
        return value;
      },
      from: function(value) {
        return value;
      }
    },

    pips: {
      mode: 'positions',
      values: [0, 50, 100],
      density: 10,
      stepped: true
    }
  });

  numSticksRange = document.getElementById('num-sticks-select');
  noUiSlider.create(numSticksRange, {
    range: {
      'min': 15,
      'max': 21
    },
    start: 17,
    step: 1,
    tooltips: true,
    format: {
      from: function(value) {
        return parseInt(value);
      },
      to: function(value) {
        return parseInt(value);
      }
    },

    pips: {
      mode: 'count',
      values: 4,
      density: 14,
      stepped: true
    }
  });

  sensitivityRange = document.getElementById('sensitivity-select');
  noUiSlider.create(sensitivityRange, {
    range: {
      'min': 0,
      'max': 40
    },
    start: 5,
    step: 1,
    tooltips: true,
    format: {
      from: function(value) {
        return parseInt(value);
      },
      to: function(value) {
        return parseInt(value);
      }
    },

    pips: {
      mode: 'positions',
      values: [0, 25, 50, 75, 100],
      density: 15,
      stepped: true
    }
  });

  numSimulationsRange = document.getElementById('num-simulations-select');
  noUiSlider.create(numSimulationsRange, {
    range: {
      'min': 0,
      'max': 500
    },
    start: 0,
    step: 50,

    tooltips: true,
    format: {
      from: function(value) {
        return parseInt(value);
      },
      to: function(value) {
        return parseInt(value);
      }
    },

    pips: {
      mode: 'positions',
      values: [0, 20, 40, 60, 80, 100],
      density: 10,
      stepped: true
    }
  });
}

/**
 * Runs the AI turn
 */
function takeAiTurn() {
  if (remainingSticks <= 0) {
    // The Player/Practice bot won, doesn't matter which bot
    endGame(isSimulation ? PLAYERS.INTELLIBOT : PLAYERS.HUMAN);
  } else {
    disableChoiceButtons();
    disableQuitButtons();
    $('#status-text').html(TXT_WAIT);
    var num = ai.takeTurn();
    if (!isSimulation) {
      // Create time lag if this is against a real player
      var lag = 1500;
      setTimeout(function() {
        applyMove(PLAYERS.AI, num);
      }, lag);
    }
    else {
      applyMove(PLAYERS.AI, num);
    }
  }
}

/**
 * Prepares appropriate buttons for the Player to take its turn.
 * If games are being simulated, takes the preset bot's turn instead
 */
function readyPlayerTurn() {
  var num;
  if (remainingSticks <= 0) {
    // The AI won
    endGame(PLAYERS.AI);
  } else if (isSimulation) {
    if (practiceOpponent == PLAYERS.AI_PRACTICE) {
      num = ai.takeTurn(true);
    } else if (practiceOpponent == PLAYERS.INTELLIBOT) {
      num = ai.takeBotTurn(true);
    } else {
      num = ai.takeBotTurn(false);
    }
    applyMove(practiceOpponent, num);
  } else {
    enableQuitButtons();
    enableChoiceButtons();
  }
}

/**
 * Applies the turn the given player chose
 */
function applyMove(player, numChosen) {
  if (player == PLAYERS.AI) {
    if (!isSimulation) {
      sticksGrid.removeSticks(numChosen);
    }
    networkTable.highlightCell(remainingSticks, numChosen, TABLE.HIGHLIGHTS.UNDECIDED);
    updateRemainingSticks(remainingSticks - numChosen);

    var format = ngettext("Nathaniel chose 1 stick.", "Nathaniel chose %(num_sticks)s sticks.", numChosen);
    var numChosenText = interpolate(format, {'num_sticks': numChosen}, true);
    $('#status-text').html(numChosenText + " " + TXT_TURN);

    readyPlayerTurn();
  } else if (player != PLAYERS.NONE) {
    if (!isSimulation) {
      sticksGrid.removeSticks(numChosen);
    }
    updateRemainingSticks(remainingSticks - numChosen);
    takeAiTurn();
  }
}

/**
 * Updates the remaining number of sticks in the AI's internal data, and the game screen
 */
function updateRemainingSticks(number) {
  remainingSticks = number;
  ai.sticksLeft = remainingSticks;
  displayBaseVariables();
}

/**
 * Runs post-match processing of match results and AI learning
 */
function endGame(winner) {
  gamesPlayed++;
  displayBaseVariables();
  if (winner == PLAYERS.AI) {
    aiWins++;
    $('#status-text').html(TXT_LOSS);
    ai.updateAI(false);
    networkTable.recolourCells(TABLE.HIGHLIGHTS.WIN);
    if (!isSimulation) {
      $('#splash-text').html(TXT_LOSS);
      $('#splash-text').removeClass('d-none');
    }
  } else if (winner != PLAYERS.NONE) {
    ai.updateAI(true);
    $('#status-text').html(TXT_WIN);
    networkTable.recolourCells(TABLE.HIGHLIGHTS.LOSS);
    if (!isSimulation) {
      $('#splash-text').html(TXT_WIN);
      $('#splash-text').removeClass('d-none');
    }
  }
  networkTable.populateTable(ai.map);
  if (!isSimulation) {
    hideChoiceButtons();
    enableQuitButtons();
    showEndButtons();
  }
}

function hideStartButtons() {
  $('#button_start').addClass('d-none');
  $('#button_modal_start').addClass('d-none');
}

function showStartButtons() {
  $('#button_start').removeClass('d-none');
  $('#button_modal_start').removeClass('d-none');
}

/**
 * Disables controls for the user to choose a number of sticks to remove.
 * Does not affect visibility
 */
function disableChoiceButtons() {
  $('#button_1').prop('disabled', true);
  $('#button_2').prop('disabled', true);
  $('#button_3').prop('disabled', true);
}

/**
 * Enables controls for the user to choose a number of sticks to remove.
 * Does not affect visibility
 */
function enableChoiceButtons() {
  $('#button_1').prop('disabled', false);
  $('#button_2').prop('disabled', false);
  $('#button_3').prop('disabled', false);
}

/**
 * Hides controls for the user to choose a number of sticks to remove
 */
function hideChoiceButtons() {
  $('#game-buttons').addClass('d-none');
}

/**
 * Shows controls for the user to choose a number of sticks to remove
 */
function showChoiceButtons() {
  $('#game-buttons').removeClass('d-none');
}

/**
 * Disables the quit buttons, does not affect visibility
 */
function disableQuitButtons() {
  $('#button_quit').prop('disabled', true);
  $('#button_modal_quit').prop('disabled', true);
}

/**
 * Enables the quit buttons, does not affect visibility
 */
function enableQuitButtons() {
  $('#button_quit').prop('disabled', false);
  $('#button_modal_quit').prop('disabled', false);
}

/**
 * Hides controls for the user to 'quit', i.e. reset the game to its page-loaded state
 */
function hideQuitButtons() {
  $('#quit-buttons').addClass('d-none');
  $('#button_modal_quit').addClass('d-none');
}

/**
 * Shows controls for the user to 'quit', i.e. reset the game to its page-loaded state
 */
function showQuitButtons() {
  $('#quit-buttons').removeClass('d-none');
  $('#button_modal_quit').removeClass('d-none');
}

/**
 * Hides controls for the user to choose to simulate matches or have a rematch
 */
function hideEndButtons() {
  $('#end-buttons').addClass('d-none');
}

/**
 * Shows controls for the user to choose to simulate matches or have a rematch
 */
function showEndButtons() {
  $('#end-buttons').removeClass('d-none');
}

/**
 * Hides the stop simulating buttons
 */
function hideCancelButtons() {
  $('#button_cancel').addClass('d-none').prop('disabled', true);
  $('#button_modal_cancel').addClass('d-none').prop('disabled', true);
}

/**
 * Shows the stop simulating buttons
 */
function showCancelButtons() {
  $('#button_cancel').removeClass('d-none').prop('disabled', false);
  $('#button_modal_cancel').removeClass('d-none').prop('disabled', false);
}