Merlin-R / Torn Blackjack Engine

// ==UserScript==
// @namespace     https://openuserjs.org/users/Merlin-R
// @name          Torn Blackjack Engine
// @description   Automatically play Torn Blackjack
// @copyright     2018, Merlin-R (https://openuserjs.org/users/Merlin-R)
// @license       MIT
// @version       1.1.0
// @include       https://www.torn.com/*
// @grant none
// ==/UserScript==

// ==OpenUserJS==
// @author Merlin-R
// ==/OpenUserJS==

/*********************************************************************************
 * Copyright 2018 Merlin Reichwald                                               *
 *                                                                               *
 * Permission is hereby granted, free of charge, to any person obtaining a copy  *
 * of this software and associated documentation files (the "Software"), to deal *
 * in the Software without restriction, including without limitation the rights  *
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell     *
 * copies of the Software, and to permit persons to whom the Software is         *
 * furnished to do so, subject to the following conditions:                      *
 *                                                                               *
 * The above copyright notice and this permission notice shall be included in    *
 * all copies or substantial portions of the Software.                           *
 *                                                                               *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR    *
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,      *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE   *
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER        *
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, *
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE *
 * SOFTWARE.                                                                     *
 *********************************************************************************/

/****************************************
 * Example Usage:                       *
 * new Blackjack(1000000,4).autoPlay(); *
 ****************************************/

class Blackjack {

  /**
   * Creates a new Blackjack Controller Engine
   * @param {number} basebet The base bet to build uppon
   * @param {number} limit   The highest chain before realizing wins
   */
  constructor(basebet, limit)
  {
    this.basebet = basebet;
    this.limit = limit;
    this.count = 0;
    this.$bj = $('.blackjack-wrap');
  }

  async playTurn() {
    await this.bet( this.decideBet() );
    while ( !(this.isGameOver()) )
      await this.perform( this.decide(), this.sumHand( this.playerHand() ) );
    if ( !this.hasWon() )
      if ( this.hasLost() )
        this.count -= 3;
      else
        this.count -= 1;
    if ( this.count < 0 )
      this.count = 0;
    await this.continueGame();
  }

  async autoPlay()
  {
    while( this.tokensLeft() )
      await this.playTurn();
  }

  tokensLeft()
  {
    return +this.$bj.find('.bj-tokens').first().text();
  }

  hasWon() {
    return !!$('.wl-msg.won').length;
  }

  hasLost() {
    return !!$('.wl-msg.lost').length;
  }

  /**
   * Confirms the action that is to be performed
   * @return {Promise} resolves after confirmation
   */
  async confirmAction() {
    this.$bj.find('.confirm-action.yes').tornClick();
    await this.wait( Blackjack.lowDelay );
  }

  /**
   * Clicks continue after a finished game
   * @return {Promise}        resolves after the button was clicked
   */
  async continueGame() {
    this.$bj.find('.continue').tornClick();
    await this.wait( Blackjack.lowDelay );
  }

  /**
   * Performs a given action
   * @param  {string}  action the action to perform
   * @return {Promise}        resolves after the action was performed
   */
  async perform( action, points ) {
    var backup = Blackjack.table[ points ];
    console.log( action );
    switch( action ) {
      case 'S': await this.clickGameButton( 'stand' );  break;
      case 'H': await this.clickGameButton( 'hit-me' ); break;
      case 'P': await this.clickGameButton( 'split', points > 11 ? 'stand' : 'hit-me', true );
                await this.continueGame();
                await this.perform( this.decide(), this.sumHand( this.playerHand() ) );
                break;
      case 'D': await this.clickGameButton( 'hit-me',/*dd*/ 'hit-me', true ); break;
      case 'F': await this.clickGameButton( 'surrender', 'hit-me', true ); break;
      case 'T':
        if ( this.canClickButton( 'split' ) && this.canClickButton( 'hit-me' /*dd*/ ) )
        {
          await this.clickGameButton( 'split', undefined, true );
          await this.wait( Blackjack.delay );
          await this.clickGameButton( 'hit-me' /*dd*/, undefined, true );
          await this.continueGame();
          await this.perform( this.decide(), this.sumHand( this.playerHand() ) );
        }
        else
        {
          await this.clickGameButton( 'split', points > 11 ? 'stand' : 'hit-me', true );
        }
        break;
    }
    await this.wait( Blackjack.delay );
  }

  async clickGameButton( button, fallback, confirm )
  {
    if ( this.canClickButton( button ) )
      this.$bj.find('#' + button + ' area').tornClick();
    else if ( fallback )
      this.$bj.find('#' + fallback + ' area').tornClick();
    else
      return;
    if ( confirm )
    {
      await this.wait( Blackjack.lowDelay );
      await this.confirmAction();
    }
  }

  canClickButton( button )
  {
    return !this.$bj.find( '#' + button ).parent().find( 'img' ).hasClass( 'disable' );
  }

  /**
   * Returns if a game is over
   * @return {Boolean} wether the game is over or not
   */
  isGameOver() {
    return !!this.$bj.find('.win-lose-wrap.bj-show').length;
  }

  /**
   * Decides the bet depending on the base bet, current session length and limit
   * @return {number} The amount to bet
   */
  decideBet() {
    if ( ++this.count > this.limit ) this.count = 1;
    return Blackjack.fib[ this.count ] * this.basebet;
  }

  /**
   * Decides how to react to a given board.
   * Possible return values are:
   * H: Hold
   * S: Stand
   * P: Split
   * D: Double Down
   * F: French / Surrender
   * T: Split, then Double Down
   * @return {string} the best reaction to the current board
   */
  decide() {
    let player = this.playerHand();
    let dealer = this.dealerHand();

    let pkey = player.join( '' );
    let dkey = dealer.length > 1 ? this.sumHand( dealer ) : dealer[ 0 ];
    
    if ( this.sumHand( player, true ) >= 18 ) return 'S';

    var row = Blackjack.table[ pkey ];
    if ( row ) return row[ dkey ];
    row = Blackjack.table[ this.sumHand( player ) ];
    if ( row ) return row[ dkey ];
    if ( this.sumHand( player ) > 19 ) return 'S';
    throw new Error("Don't know what to do with hand "+pkey+" against "+dkey);
  }

  /**
   * Sum all points in a hand
   * @param  {string[]} hand the hand to sum
   * @param  {boolean=} high form the high sum (Aces count 11 or 1)
   * @return {number}       the calculated sum of points
   */
  sumHand( hand, high ) {
    let result = 0;
    hand.forEach( card => { if ( card === 'X' ) result += 10; else if ( card === 'A' ) result += high ? 11 : 1; else result += +card });
    return result;
  }

  /**
   * get a list of card identifiers for the dealer hand
   * @return {string[]} the card identifiers
   */
  dealerHand() {
    return this.hand('.dealer-cards');
  }

  /**
   * get a list of card identifiers for the player hand
   * @return {string[]} the card identifiers
   */
  playerHand() {
    return this.hand('.player-cards');
  }

  /**
   * get a list of card identifiers for the given board class
   * @param  {string} cls the board css class
   * @return {string[]}   the card identifiers
   */
  hand( cls ) {
    return this.formatHand($(cls + ' .table-cards-wrap .cards .inplace:not(.card-back)').map((i,e) => $(e).attr('class').split(' ').filter( cls => cls !== 'inplace' )[ 0 ]));
  }

  /**
   * Formats CSS Classes for torn cards to single letter card descriptors
   * @param  {string[]} hand The hand to be formatted
   * @return {string[]}      the formatted hand
   */
  formatHand( hand ) {
    return Array.from( hand ).map( cardClass => cardClass.substr( 5 ).replace(/(spades|clubs|hearts|diamonds)-/g,'').replace( /(J|Q|K|10)/g, 'X' ) ).sort().reverse();
  }

  /**
   * Bet a given amount and start the game
   * @param  {number}  amount The amount of money to bet
   * @return {Promise}        Resolves after game is started.
   */
  async bet( amount ) {
    this.$bj.find('input.bet').tornInput( amount );
    await this.wait( Blackjack.lowDelay );
    await this.confirm();
  }

  /**
   * Confirms the game start dialog.
   * @return {Promise} Resolves after confirmation
   */
  async confirm() {
    this.$bj.find( '.bet-confirm' ).find( '.yes' ).tornClick();
    await this.wait( Blackjack.delay );
  }

  /**
   * Waits a given amount of time before resolving
   * @param  {number} ms  The amount of milliseconds to wait for
   * @param  {T=}     ret An obtional return value to resolve with
   * @return {Promise<T>} The return value if one was given
   * @template T
   */
  wait( ms, ret ) {
    return new Promise( resolve => {
      setTimeout( resolve.bind( this, ret ), ms );
    });
  }


}


Blackjack.lowDelay = 5000;
Blackjack.delay    = 10000;

/**
 * Blackjack Constants
 * @type {Object.<string,Object<string,string>>}
 */
Blackjack.table = {"5":{"2":"H","3":"H","4":"H","5":"H","6":"H","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"5"},"6":{"2":"H","3":"H","4":"H","5":"H","6":"H","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"6"},"7":{"2":"H","3":"H","4":"H","5":"H","6":"H","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"7"},"8":{"2":"H","3":"H","4":"H","5":"H","6":"H","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"8"},"9":{"2":"H","3":"D","4":"D","5":"D","6":"D","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"9"},"11":{"2":"D","3":"D","4":"D","5":"D","6":"D","7":"D","8":"D","9":"D","X":"D","A":"H","KEY":"11"},"12":{"2":"H","3":"H","4":"S","5":"S","6":"S","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"12"},"13":{"2":"S","3":"S","4":"S","5":"S","6":"S","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"13"},"14":{"2":"S","3":"S","4":"S","5":"S","6":"S","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"14"},"15":{"2":"S","3":"S","4":"S","5":"S","6":"S","7":"H","8":"H","9":"H","X":"F","A":"H","KEY":"15"},"16":{"2":"S","3":"S","4":"S","5":"S","6":"S","7":"H","8":"H","9":"F","X":"F","A":"F","KEY":"16"},"17":{"2":"S","3":"S","4":"S","5":"S","6":"S","7":"S","8":"S","9":"S","X":"S","A":"S","KEY":"17"},"18":{"2":"S","3":"S","4":"S","5":"S","6":"S","7":"S","8":"S","9":"S","X":"S","A":"S","KEY":"18"},"19":{"2":"S","3":"S","4":"S","5":"S","6":"S","7":"S","8":"S","9":"S","X":"S","A":"S","KEY":"19"},"22":{"2":"T","3":"T","4":"P","5":"P","6":"P","7":"P","8":"S","9":"S","X":"S","A":"S","KEY":"22"},"33":{"2":"T","3":"T","4":"P","5":"P","6":"P","7":"P","8":"H","9":"H","X":"H","A":"H","KEY":"33"},"44":{"2":"H","3":"H","4":"H","5":"T","6":"T","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"44"},"55":{"2":"D","3":"D","4":"D","5":"D","6":"D","7":"D","8":"D","9":"D","X":"H","A":"H","KEY":"55"},"66":{"2":"T","3":"P","4":"P","5":"P","6":"P","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"66"},"77":{"2":"P","3":"P","4":"P","5":"P","6":"P","7":"P","8":"H","9":"H","X":"H","A":"H","KEY":"77"},"88":{"2":"P","3":"P","4":"P","5":"P","6":"P","7":"P","8":"P","9":"P","X":"P","A":"P","KEY":"88"},"99":{"2":"P","3":"P","4":"P","5":"P","6":"P","7":"S","8":"P","9":"P","X":"S","A":"S","KEY":"99"},"10":{"2":"D","3":"D","4":"D","5":"D","6":"D","7":"D","8":"D","9":"D","X":"H","A":"H","KEY":"X"},"A2":{"2":"H","3":"H","4":"H","5":"D","6":"D","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"A2"},"A3":{"2":"H","3":"H","4":"H","5":"D","6":"D","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"A3"},"A4":{"2":"H","3":"H","4":"D","5":"D","6":"D","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"A4"},"A5":{"2":"H","3":"H","4":"D","5":"D","6":"D","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"A5"},"A6":{"2":"H","3":"D","4":"D","5":"D","6":"D","7":"H","8":"H","9":"H","X":"H","A":"H","KEY":"A6"},"A7":{"2":"S","3":"D","4":"D","5":"D","6":"D","7":"S","8":"S","9":"H","X":"H","A":"H","KEY":"A7"},"A8":{"2":"S","3":"S","4":"S","5":"S","6":"S","7":"S","8":"S","9":"S","X":"S","A":"S","KEY":"A8"},"A9":{"2":"S","3":"S","4":"S","5":"S","6":"S","7":"S","8":"S","9":"S","X":"S","A":"S","KEY":"A9"},"XX":{"2":"S","3":"S","4":"S","5":"S","6":"S","7":"S","8":"S","9":"S","X":"S","A":"S","KEY":"XX"},"AA":{"2":"P","3":"P","4":"P","5":"P","6":"P","7":"P","8":"P","9":"P","X":"P","A":"P","KEY":"AA"},"XA":{"2":"S","3":"S","4":"S","5":"S","6":"S","7":"S","8":"S","9":"S","X":"S","A":"S","KEY":"AX"}};
/**
 * Fibonacci Sequence
 * @type {number[]}
 */
Blackjack.fib = [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073, 4807526976, 7778742049, 12586269025, 20365011074, 32951280099, 53316291173, 86267571272, 139583862445, 225851433717, 365435296162, 591286729879, 956722026041, 1548008755920, 2504730781961, 4052739537881, 6557470319842, 10610209857723, 17167680177565, 27777890035288, 44945570212853, 72723460248141, 117669030460994, 190392490709135, 308061521170129, 498454011879264, 806515533049393, 1304969544928657, 2111485077978050 ];

// Torn Utility Functions
$.fn.tornInput = function (value) { return $(this).val(value).each((i, e) => { e.dispatchEvent(new CustomEvent("input", { bubbles: true })); e.dispatchEvent(new CustomEvent('keyup',{bubbles:true})); }); }
$.fn.tornClick = function () { return $(this).each((i, e) => e.dispatchEvent(new CustomEvent("click", { bubbles: true }))); }

window.Blackjack = Blackjack;
setTimeout(function(){
  var btnAuto = $('<a class="action-btn-wrap"><div class="action-btn left" /><div class="action-btn right" />AUTO<div class="clear" /></a>');
  btnAuto.click(() => {
    var bet = +$('.blackjack-wrap input.bet').val();
    new Blackjack( bet, 4 ).autoPlay();
  })
  $('.blackjack-wrap .bet-action').append( btnAuto );
}, 5000);