import type { Character , MatchRecord , Bettor } from "./records" ; import { Updater } from "./records" ; import { Settings } from "./settings" ; import { Match } from "./tracker" ; import { Observer , Lunatic , Cowboy , Scientist , Chromosome } from "./strategy" ; import { binaryInsertByProperty } from "./utils" ; import { Globals } from "./globals" ; class StatusScanner { announcements : string [ ] = [ ] ; readonly winIndicator = " wins" ; constructor ( ) { const status = document . querySelector ( "#betstatus" ) ; const observer = new MutationObserver ( ( ) => { this . announcements . push ( status . innerHTML ) ; observer . takeRecords ( ) ; } ) ; observer . observe ( status , { subtree : true , childList : true , attributes : true , } ) ; } getAnnouncements ( preserve ? ) { const copy = this . announcements . slice ( 0 ) ; if ( ! preserve ) { this . announcements = [ ] ; } return copy ; } getWinner ( ) { const recent = this . getAnnouncements ( ) ; recent . reverse ( ) ; for ( const announcement of recent ) { if ( announcement . includes ( this . winIndicator ) ) { return announcement . split ( this . winIndicator ) [ 0 ] ; } } return null ; } } export function isTournament ( ) { const modeInfo = document . querySelector ( "#footer-alert" ) . innerHTML ; return modeInfo . includes ( "bracket" ) || modeInfo . includes ( "FINAL ROUND" ) || modeInfo . includes ( "Tournament mode start" ) ; } export class Controller { statusScanner : StatusScanner ; currentMatch : Match ; infoFromWaifu : { c1 : string ; c2 : string ; tier : string ; mode : string ; } [ ] ; lastWinnerFromWaifuAnnouncement : string ; odds : string ; crowdFavor : number ; illumFavor : number ; ticksSinceMatchBegan : number ; bestChromosome : Chromosome ; nextStrategy : string ; bettorsC1 : [ string , boolean ] [ ] ; bettorsC2 : [ string , boolean ] [ ] ; settings : Settings ; lastMatchCumulativeBetTotal : number ; savedVideo : JQuery < HTMLElement > ; lastFooterMessage : string ; charactersV1 : Character [ ] ; bettorsV1 : Bettor [ ] ; bettingAvailable = false ; bettingEntered = false ; bettingComplete = true ; matchesBeforeReset = 100 ; matchesProcessed = 0 ; attemptsToProcess = 0 ; readonly debugMode = true ; readonly maxAttempts = 3 ; readonly timerInterval = 3000 ; constructor ( ) { this . currentMatch = null ; this . statusScanner = new StatusScanner ( ) ; this . infoFromWaifu = [ ] ; this . lastWinnerFromWaifuAnnouncement = null ; this . odds = null ; this . crowdFavor = 2 ; this . illumFavor = 2 ; this . ticksSinceMatchBegan = - 999 ; this . bestChromosome = null ; this . nextStrategy = "o" ; this . bettorsC1 = [ ] ; this . bettorsC2 = [ ] ; this . settings = null ; this . lastMatchCumulativeBetTotal = null ; this . savedVideo = null ; this . lastFooterMessage = document . querySelector ( "#footer-alert" ) ? document . querySelector ( "#footer-alert" ) . innerHTML : null ; this . charactersV1 = [ ] ; this . bettorsV1 = [ ] ; setInterval ( ( ) => this . run ( ) , this . timerInterval ) ; } run ( ) { if ( ! this . settings ) { return ; } this . ticksSinceMatchBegan += 1 ; const bettingTable = document . querySelector ( "#wager" ) ; const styleObj = window . getComputedStyle ( bettingTable , null ) ; this . bettingAvailable = styleObj . display !== "none" && document . querySelector ( "#footer-alert" ) . innerHTML !== this . lastFooterMessage ; if ( this . bettingAvailable && this . bettingComplete ) { this . bettingEntered = false ; this . bettingComplete = false ; } if ( this . bettingAvailable && ! this . bettingEntered ) { this . bettingEntered = true ; if ( this . currentMatch !== null ) { let winner = this . statusScanner . getWinner ( ) ; if ( winner === null ) { winner = this . lastWinnerFromWaifuAnnouncement ; } if ( winner !== this . currentMatch . names [ 0 ] && winner !== this . currentMatch . names [ 1 ] ) { winner = null ; } if ( winner === null && this . attemptsToProcess < this . maxAttempts ) { this . attemptsToProcess += 1 ; return ; } if ( winner !== null ) { this . attemptsToProcess = 0 ; this . currentMatch . update ( this . infoFromWaifu , this . odds , { ticks : this . ticksSinceMatchBegan , interval : this . timerInterval , } , this . crowdFavor , this . illumFavor ) ; const records = this . currentMatch . getRecords ( winner ) ; const mr = records [ 0 ] as MatchRecord ; const c1 = records [ 1 ] as Character ; const c2 = records [ 2 ] as Character ; let predictionString = null ; switch ( mr . pw ) { case "a" : predictionString = "Did not bet" ; break ; case "t" : predictionString = "Predicted correctly" ; break ; case "f" : predictionString = "Predicted incorrectly" ; break ; } console . log ( `Match result:
Character 1: ${ mr . c1 }
Character 2: ${ mr . c2 }
Winner: ${ ( mr . w == 0 ? mr . c1 : mr . c2 ) }
Strategy: ${ mr . sn }
Prediction: ${ predictionString }
Tier: ${ mr . t }
Mode: ${ mr . m }
Odds: ${ mr . o }
Length of match in seconds: ${ mr . ts }` ) ; if ( ! Globals . dirtyRecords ) { this . updateRecords ( { bettors_v1 : this . bettorsV1 , characters_v1 : this . charactersV1 , } , mr , c1 , c2 ) ; } else { chrome . storage . local . get ( [ "characters_v1" , "bettors_v1" ] , ( results ) => { this . updateRecords ( results , mr , c1 , c2 ) ; Globals . dirtyRecords = false ; } ) ; } } else { console . log ( `- failed to determine winner, matches this cycle: ${ this . matchesProcessed }` ) ; if ( this . matchesProcessed >= this . matchesBeforeReset ) { location . reload ( ) ; } } } this . lastFooterMessage = document . querySelector ( "#footer-alert" ) . innerHTML ; const tournament = isTournament ( ) ; if ( this . matchesProcessed === 0 && this . bestChromosome === null ) { this . currentMatch = new Match ( new Observer ( ) ) ; } else if ( ! tournament && this . settings . limit_enabled && this . currentMatch && this . currentMatch . getBalance ( ) >= this . settings . limit ) { console . log ( `- limit of ${ this . settings . limit } is reached, observing` ) ; this . currentMatch = new Match ( new Observer ( ) ) ; } else if ( tournament && this . settings . tourneyLimit_enabled && this . currentMatch && this . currentMatch . getBalance ( ) >= this . settings . tourneyLimit ) { console . log ( `- tourney limit of ${ this . settings . limit } is reached, observing` ) ; this . currentMatch = new Match ( new Observer ( ) ) ; } else { let level ; if ( this . currentMatch && this . currentMatch . strategy ) { level = this . currentMatch . strategy . level ; } else { const nullMatch = new Match ( new Observer ( ) ) ; nullMatch . strategy . adjustLevel ( nullMatch . getBalance ( ) ) ; level = nullMatch . strategy . level ; } switch ( this . settings . nextStrategy ) { case "o" : this . currentMatch = new Match ( new Observer ( ) ) ; break ; case "rc" : this . currentMatch = new Match ( new Cowboy ( ) ) ; break ; case "cs" : this . currentMatch = new Match ( new Scientist ( this . bestChromosome , level ) ) ; break ; case "ipu" : this . currentMatch = new Match ( new Lunatic ( level ) ) ; break ; default : this . currentMatch = new Match ( new Observer ( ) ) ; break ; } const modeInfo = this . lastFooterMessage ; if ( tournament ) { this . currentMatch . mode = "t" ; } else if ( modeInfo . toUpperCase ( ) . includes ( "EXHIBITION" ) ) { this . currentMatch . mode = "e" ; } else if ( modeInfo . includes ( "until the next tournament" ) ) { this . currentMatch . mode = "m" ; } else { this . currentMatch . mode = "U" ; } if ( this . settings . aggro_enabled && this . currentMatch . getBalance ( ) < this . settings . aggro_limit ) { this . currentMatch . setAggro ( true ) ; } else { this . currentMatch . setAggro ( false ) ; } if ( ! tournament && this . settings . upsetBetting_enabled && this . currentMatch . getBalance ( ) < this . settings . upsetBetting_limit ) { this . currentMatch . setMaximum ( true ) ; this . currentMatch . upsetMode = true ; } else { this . currentMatch . setMaximum ( false ) ; } } if ( ( this . currentMatch . mode === "U" || this . currentMatch . mode === "e" ) && ( this . currentMatch . names [ 0 ] . toLowerCase ( ) . includes ( "team" ) || this . currentMatch . names [ 1 ] . toLowerCase ( ) . includes ( "team" ) ) ) { this . currentMatch = null ; console . log ( "- skipping team match" ) ; } else if ( this . currentMatch . mode . startsWith ( "e" ) && this . settings . betOnExhibitions !== undefined && ! this . settings . betOnExhibitions ) { this . currentMatch = null ; console . log ( "- skipping exhibition match because it is deactivated" ) ; } else if ( this . currentMatch . names [ 0 ] === this . currentMatch . names [ 1 ] ) { this . currentMatch = null ; console . log ( "- skipping mirror match" ) ; } else { this . currentMatch . names [ 0 ] . replace ( /,/g , "_" ) ; this . currentMatch . names [ 1 ] . replace ( /,/g , "_" ) ; this . currentMatch . init ( ) ; } this . matchesProcessed += 1 ; } if ( ! this . bettingAvailable && this . bettingEntered ) { this . bettingComplete = true ; } } updateRecords ( results , mr , c1 , c2 ) { let characters = [ ] ; let bettors = [ ] ; chrome . runtime . sendMessage ( { data : mr , query : "addMatchRecord" } ) ; if ( results . characters_v1 ) { characters = results . characters_v1 ; } binaryInsertByProperty ( c1 , characters , "name" ) ; binaryInsertByProperty ( c2 , characters , "name" ) ; if ( results . bettors_v1 ) { bettors = results . bettors_v1 ; } const updater = new Updater ( ) ; const namesOfBettorsWhoAlreadyHaveRecords = [ ] ; for ( const bettor of bettors ) { namesOfBettorsWhoAlreadyHaveRecords . push ( bettor . name ) ; } const bc1 = [ ] ; const bc2 = [ ] ; for ( const bettor of this . bettorsC1 ) { const b = updater . getBettor ( bettor [ 0 ] , bettors , namesOfBettorsWhoAlreadyHaveRecords ) ; b . type = ( bettor [ 1 ] ) ? "i" : "c" ; bc1 . push ( b ) ; } for ( const bettor of this . bettorsC2 ) { const b = updater . getBettor ( bettor [ 0 ] , bettors , namesOfBettorsWhoAlreadyHaveRecords ) ; b . type = ( bettor [ 1 ] ) ? "i" : "c" ; bc2 . push ( b ) ; } updater . updateBettorsFromMatch ( mr , bc1 , bc2 ) ; if ( this . debugMode ) { console . log ( `- number of:: chars: ${ characters . length }, bettors: ${ bettors . length }` ) ; } const mbr = this . matchesBeforeReset ; const mp = this . matchesProcessed ; this . bettorsV1 = bettors ; this . charactersV1 = characters ; chrome . storage . local . set ( { bettors_v1 : bettors , characters_v1 : characters , } , ( ) => { if ( this . debugMode ) { console . log ( `- records saved, matches this cycle: ${ mp }` ) ; } if ( mp >= mbr ) { location . reload ( ) ; } } ) ; } ensureTwitch ( ) { chrome . runtime . sendMessage ( chrome . runtime . id , { getTwitch : true , } , function ( ) : void { } ) ; } removeVideoWindow ( ) { const parent = $ ( "#video-embed" ) ; this . savedVideo = parent . clone ( true ) ; parent . empty ( ) ; } enableVideoWindow ( ) { const embeddedVideo = $ ( "#video-embed" ) ; if ( this . savedVideo && embeddedVideo [ 0 ] . childNodes . length === 0 ) { embeddedVideo . remove ( ) ; this . savedVideo . appendTo ( $ ( "#stream" ) ) ; this . savedVideo = null ; } } toggleVideoWindow ( ) { this . settings . video = ! this . settings . video ; if ( ! this . settings . video ) { this . removeVideoWindow ( ) ; } else { this . enableVideoWindow ( ) ; } this . saveSettings ( "- settings updated, video: " + ( this . settings . video ? "true" : "false" ) ) ; } setAggro ( aggro_enabled , aggro_limit ) { if ( aggro_limit === this . settings . aggro_limit && aggro_enabled === this . settings . aggro_enabled ) { return ; } if ( aggro_limit ) { this . settings . aggro_limit = aggro_limit ; } this . settings . aggro_enabled = aggro_enabled ; this . saveSettings ( `- settings updated, talimit ${ ( aggro_enabled ? "enabled" : "disabled" ) } limit : ${ aggro_limit }` ) ; } setExhibitions ( value ) { this . settings . betOnExhibitions = value ; this . saveSettings ( "- settings updated, exhibition betting: " + ( this . settings . betOnExhibitions ? "true" : "false" ) ) ; } setAllInTournament ( value ) { this . settings . allInTourney = value ; this . saveSettings ( "- settings updated, go all in at tournaments: " + ( this . settings . allInTourney ? "true" : "false" ) ) ; } setMultiplier ( value ) { this . settings . multiplier = value ; this . saveSettings ( `- settings updated, multiplier: ${ value }` ) ; } setLimit ( enabled , limit ) { if ( limit === this . settings . limit && enabled === this . settings . limit_enabled ) { return ; } if ( limit ) { this . settings . limit = limit ; } this . settings . limit_enabled = enabled ; this . saveSettings ( `- settings updated, limit ${ ( enabled ? "enabled" : "disabled" ) } + " limit : ${ limit }` ) ; } setTourneyLimit ( enabled , limit ? ) { if ( limit === this . settings . tourneyLimit && enabled === this . settings . tourneyLimit_enabled ) { return ; } if ( limit ) { this . settings . tourneyLimit = limit ; } this . settings . tourneyLimit_enabled = enabled ; this . saveSettings ( `- settings updated, tourney limit ${ ( enabled ? "enabled" : "disabled" ) } + " limit : ${ limit }` ) ; } setUpsetBetting ( upsetBetting_enabled , upsetBetting_limit ) { if ( upsetBetting_limit === this . settings . upsetBetting_limit && upsetBetting_enabled === this . settings . upsetBetting_enabled ) { return ; } if ( upsetBetting_limit ) { this . settings . upsetBetting_limit = upsetBetting_limit ; } this . settings . upsetBetting_enabled = upsetBetting_enabled ; this . saveSettings ( `- settings updated, upset mode ${ ( upsetBetting_enabled ? "enabled" : "disabled" ) } limit : ${ upsetBetting_limit }` ) ; } setMaximumBetAmount ( enabled , limit ) { if ( limit === this . settings . maximumBetAmount_limit && enabled === this . settings . maximumBetAmount_enabled ) { return ; } this . settings . maximumBetAmount_limit = limit ; this . settings . maximumBetAmount_enabled = enabled ; this . saveSettings ( `- settings updated, maximum bet amount ${ ( enabled ? "enabled" : "disabled" ) } limit: ${ limit }` ) ; } changeStrategy ( sn , data ? ) { let t = "" ; switch ( sn ) { case "cs_o" : this . settings . nextStrategy = "o" ; t = "Monk" ; break ; case "cs_rc" : this . settings . nextStrategy = "rc" ; t = "Cowboy" ; break ; case "cs_cs" : this . settings . nextStrategy = "cs" ; this . bestChromosome = new Chromosome ( ) . loadFromJSON ( data ) ; t = "Scientist" ; break ; case "cs_ipu" : this . settings . nextStrategy = "ipu" ; t = "Lunatic" ; break ; } console . log ( "- changing strategy to " + t ) ; this . saveSettings ( "- settings saved" ) ; } receiveBestChromosome ( data ) { this . bestChromosome = new Chromosome ( ) . loadFromJSON ( data ) ; } setKeepAlive ( value ) { this . settings . keepAlive = value ; this . saveSettings ( "- settings updated, always keep saltybet tab alive: " + ( this . settings . keepAlive ? "true" : "false" ) ) ; } saveSettings ( msg ) { chrome . storage . local . set ( { settings_v1 : this . settings , } , function ( ) : void { console . log ( msg ) ; } ) ; } } function prepareJQueryDialog ( ) { $ ( 'link[href="../css/jquery-ui-1.11.min.css"]' ) . prop ( "disabled" , true ) ; const link = '<link rel="stylesheet" type="text/css" href="' + chrome . extension . getURL ( "css/jquery-ui.css" ) + '">' ; $ ( "head" ) . append ( link ) ; const wrapper = document . getElementById ( "wrapper" ) ; const messageDialogue = document . createElement ( "div" ) ; messageDialogue . setAttribute ( "id" , "dialog" ) ; wrapper . appendChild ( messageDialogue ) ; let dialogTimer = null ; $ ( "#dialog" ) . dialog ( { autoOpen : false , title : "Saltbot Notification" , open ( ) { if ( dialogTimer !== null ) { clearTimeout ( dialogTimer ) ; } const dia = $ ( this ) ; dialogTimer = setTimeout ( function ( ) { dia . dialog ( "close" ) ; } , 5000 ) ; } , } ) ; } function correctChatIFrame ( ) { const chatFrame = ( document . getElementById ( "chat-frame-stream" ) as HTMLIFrameElement ) ; if ( chatFrame . src === "http://twitch.tv/saltybet/chat" ) { chatFrame . src = "http://twitch.tv/embed/saltybet/chat" ; } } chrome . runtime . sendMessage ( { browserAction : true , } , function ( ) : void { console . debug ( "Activated browser action" ) ; } ) ; if ( window . location . href === "http://www.saltybet.com/" || window . location . href === "http://mugen.saltybet.com/" || window . location . href === "https://www.saltybet.com/" || window . location . href === "https://mugen.saltybet.com/" ) { correctChatIFrame ( ) ; Globals . ctrl = new Controller ( ) ; const ctrl = Globals . ctrl ; ctrl . ensureTwitch ( ) ; chrome . storage . local . get ( [ "settings_v1" ] , function ( results ) { if ( results . settings_v1 ) { ctrl . settings = results . settings_v1 ; const defaultSettings = new Settings ( ) ; if ( ! ctrl . settings . video ) { ctrl . removeVideoWindow ( ) ; } for ( const setting in ctrl . settings ) { if ( ctrl . settings . hasOwnProperty ( setting ) ) { if ( ctrl . settings [ setting ] === undefined ) { ctrl . settings [ setting ] = defaultSettings [ setting ] ; } } } ctrl . saveSettings ( "- settings upgraded" ) ; } else { ctrl . settings = new Settings ( ) ; ctrl . settings . nextStrategy = "o" ; ctrl . saveSettings ( "- settings initialized" ) ; } console . log ( "- settings applied" ) ; } ) ; chrome . runtime . onMessage . addListener ( function ( message ) { if ( typeof message === "string" ) { const winMessageIndicator = " wins" ; const newMatchIndicator = "Bets are OPEN for " ; const betsLockedIndicator = "Bets are locked" ; if ( message . includes ( newMatchIndicator ) ) { const regex = /(?:Bets are OPEN for )(.*)(?: vs )(.*)(?:! \()(X|S|A|B|P|NEW)(?: Tier\))(.*)/g ; let matches = regex . exec ( message ) ; if ( matches === null ) { const regexLoose = /(?:Bets are OPEN for )(.*)(?: vs )(.*)!(.*)/g ; matches = regexLoose . exec ( message ) ; matches . push ( matches [ 3 ] ) ; matches [ 3 ] = "U" ; } if ( matches [ 3 ] === "NEW" ) { matches [ 3 ] = "U" ; } if ( matches [ 4 ] . includes ( "matchmaking" ) ) { matches [ 4 ] = "m" ; } else if ( matches [ 4 ] . includes ( "tournament" ) ) { matches [ 4 ] = "t" ; } else if ( matches [ 4 ] . includes ( "exhibition" ) ) { matches [ 4 ] = "e" ; } matches [ 1 ] . replace ( /,/g , "_" ) ; matches [ 2 ] . replace ( /,/g , "_" ) ; ctrl . infoFromWaifu . push ( { c1 : matches [ 1 ] , c2 : matches [ 2 ] , tier : matches [ 3 ] , mode : matches [ 4 ] , } ) ; while ( ctrl . infoFromWaifu . length > 2 ) { ctrl . infoFromWaifu . splice ( 0 , 1 ) ; } } else if ( message . includes ( winMessageIndicator ) ) { ctrl . lastWinnerFromWaifuAnnouncement = message . split ( winMessageIndicator ) [ 0 ] ; } else if ( message . includes ( betsLockedIndicator ) ) { ctrl . ticksSinceMatchBegan = 0 ; setTimeout ( function ( ) { try { const oddsBox = document . querySelector ( "#lastbet" ) ; const c1Odds = ( oddsBox . childNodes [ oddsBox . childNodes . length - 3 ] as HTMLElement ) . innerHTML ; const c2Odds = ( oddsBox . childNodes [ oddsBox . childNodes . length - 1 ] as HTMLElement ) . innerHTML ; ctrl . odds = "" + c1Odds + ":" + c2Odds ; } catch ( e ) { ctrl . odds = null ; } try { const moneyText = document . querySelector ( "#odds" ) . innerHTML . replace ( /,/g , "" ) ; const regex = /\$([0-9]*)/g ; if ( regex . test ( moneyText ) ) { const mtMatches = moneyText . match ( regex ) ; ctrl . lastMatchCumulativeBetTotal = parseInt ( mtMatches [ 0 ] . replace ( "$" , "" ) , 10 ) + parseInt ( mtMatches [ 1 ] . replace ( "$" , "" ) , 10 ) ; } else { throw new Error ( "totals error" ) ; } } catch ( e ) { ctrl . lastMatchCumulativeBetTotal = null ; } const betsForC1 = document . querySelector ( "#sbettors1" ) ; const betsForC2 = document . querySelector ( "#sbettors2" ) ; try { const crowdSizeC1 = $ ( betsForC1 ) . find ( ".bettor-line" ) . length ; const crowdSizeC2 = $ ( betsForC2 ) . find ( ".bettor-line" ) . length ; const illumSizeC1 = $ ( betsForC1 ) . find ( ".goldtext" ) . length ; const illumSizeC2 = $ ( betsForC2 ) . find ( ".goldtext" ) . length ; if ( crowdSizeC1 === crowdSizeC2 ) { ctrl . crowdFavor = 2 ; } else { ctrl . crowdFavor = ( crowdSizeC1 > crowdSizeC2 ) ? 0 : 1 ; } if ( illumSizeC1 === illumSizeC2 ) { ctrl . illumFavor = 2 ; } else { ctrl . illumFavor = ( illumSizeC1 > illumSizeC2 ) ? 0 : 1 ; } } catch ( e ) { ctrl . crowdFavor = 2 ; ctrl . illumFavor = 2 ; } try { const crowdC1 = $ ( betsForC1 ) . find ( ".bettor-line" ) ; const crowdC2 = $ ( betsForC2 ) . find ( ".bettor-line" ) ; ctrl . bettorsC1 = [ ] ; ctrl . bettorsC2 = [ ] ; crowdC1 . each ( function ( ) { const e = $ ( this ) . find ( "strong" ) [ 0 ] ; ctrl . bettorsC1 . push ( [ e . innerHTML , e . classList . contains ( "goldtext" ) ] ) ; } ) ; crowdC2 . each ( function ( ) { const e = $ ( this ) . find ( "strong" ) [ 0 ] ; ctrl . bettorsC2 . push ( [ e . innerHTML , e . classList . contains ( "goldtext" ) ] ) ; } ) ; } catch ( e ) { ctrl . bettorsC1 = [ ] ; ctrl . bettorsC2 = [ ] ; } } , 10000 ) ; } } } ) ; setInterval ( ( ) => { ctrl . ensureTwitch ( ) ; } , 60000 ) ; } window . addEventListener ( "beforeunload" , function ( ) { Globals . ctrl . enableVideoWindow ( ) ; } ) ; prepareJQueryDialog ( ) ; export function displayDialogMessage ( message ) { const dialog = $ ( "#dialog" ) ; dialog . html ( message . replace ( /\n/g , "<br />" ) ) ; dialog . dialog ( "open" ) ; }