|
// ==UserScript== |
|
// @name Blaseball Helper |
|
// @namespace https://gist.github.com/Eibwen/ |
|
// @version 1.4.2 |
|
// @description try to take over the world (of Blaseball)! |
|
// @updateURL https://gist.github.com/Eibwen/012ec42416dd4749d5f4e0db6b162ef5/raw/BlaseballHelper.user.js |
|
// @author Greg Walker |
|
// @match https://www.blaseball.com/* |
|
// @grant GM_addStyle |
|
// @require https://raw.githubusercontent.com/jakesgordon/javascript-state-machine/master/dist/state-machine.min.js |
|
// ==/UserScript== |
|
|
|
/* VERISON NOTES |
|
This version is mostly exploring new things, and trying to make newer features more reliable |
|
|
|
Need to do: |
|
- Switch over to new betting state machine (and debug any of that) |
|
- implemnet a GetHashCode in the Unique events |
|
- Work on parsing the GameWidget cards for each screen |
|
- Hook up better fallbacks to make sure betting always happens |
|
*/ |
|
|
|
/* |
|
Current features: |
|
- Parse and store all star ratings for teams (need a feature to pause this) |
|
- Inject that info into the page |
|
- Change color of the won/lost toast notifications |
|
- Control panel to be able to control things |
|
- Auto-place bids |
|
- At auto-bid time (auto-bid when there is 20 mins left (default), so that all money is rectified) |
|
- Script enforces not being on a page other than `/upcoming` for more than a minute (will redirect you back) (TODO ability to disable this? Or warn?) |
|
- Control panel (stubbed out anyway) |
|
- Time to auto-bid (T-20, or later/earlier?) |
|
- Bid amount strategy |
|
|
|
|
|
Future plans: |
|
- Track game result history, along with team data at the time (to determine algorithm ideas) |
|
- Track bid history automatically (to determine what algorithms might be better) |
|
- CalculateBet (or bid) should take into account the number of games and the money balance, and reduce the bet if we can't bet on all the games |
|
- Also should know how to request the "Begging" option in the shop when the balance hits 0 |
|
- When low on money (is where it matters), place bets by most "likely" to win first |
|
*/ |
|
|
|
|
|
/* Record keeping: |
|
Messages like: |
|
>The underdog Shoe Thieves won the game. |
|
>You bet 60 on the Shoe Thieves and won 122. |
|
|
|
Augment data with: |
|
- Both teams in the game |
|
- Actual percentage data for the game |
|
- [x] Team Star data for each team |
|
- breakdown to each player?? |
|
- Score for each team |
|
- Weather for the game |
|
|
|
Event types to send via slackDataCollection: |
|
- [x] team-data-changed (has old and new team data) |
|
- game-upcoming (has percentages, teamIds+teamName, weather, gameNumber) |
|
- Use a `new Map()` to keep track of what has been sent |
|
- game-results (has percentages, teamIds+teamName, weather, gameNumber, betAmount, score) |
|
- Use a `new Map()` to keep track of what has been sent |
|
- [x] bet-placed (has amount, teamId/teamName, strategy) |
|
- This is ONLY auto-bets??? |
|
- [x] bet-result (from toast, has amount, teamName, result) |
|
- [x] Use a `new Map()` to keep track of what has been sent (but also check for duplicates in the input list) |
|
- [x-log] auto-bet-triggered (has datetime?, and countdown left) |
|
- [x] countdown-done (has datetime?, and countdown left) |
|
- [x] game-results-bets (has summarized results of the "day") |
|
*/ |
|
|
|
|
|
|
|
// ########## Global Settings ########## // |
|
|
|
const mainLoopSpeed = 2000; |
|
let mainLoopIntervalRef = null; |
|
|
|
// TODO inject control buttons to diable this, as if it breaks it is super annoying |
|
let automaticTeamParsingEnabled = true; |
|
|
|
(function() { |
|
'use strict'; |
|
|
|
console.log("INFO: Booting up..."); |
|
slackLogCollection('INFO', 'booting up'); |
|
mainLoopIntervalRef = setInterval(mainLoop, mainLoopSpeed); |
|
})(); |
|
|
|
// ########## Main Call Loop ########## // |
|
|
|
let refreshingPage = false; |
|
function mainLoop() { |
|
var url = document.URL; |
|
|
|
InjectControlPanel(); |
|
|
|
if (automaticTeamParsingEnabled && url.indexOf('team') >= 0) { |
|
ParseTeamDataScreen(); |
|
} |
|
|
|
if (IsErrorPage() && !refreshingPage) { |
|
refreshingPage = true; |
|
|
|
var errorCount = localStorage.getItem("errorPageCount") || 0; |
|
errorCount++; |
|
|
|
var seconds = Math.min(600, errorCount * errorCount * 5); |
|
console.log(`WARN: Detected error page, refreshing in ${seconds} seconds (for ${errorCount})`); |
|
|
|
localStorage.setItem("errorPageCount", errorCount); |
|
setTimeout(function(){ |
|
console.log(`FATAL: Triggering reload, error count is at: ${errorCount}`); |
|
location.reload(); |
|
}, seconds*1000); |
|
|
|
// Be nice and disable a few other things: |
|
teamParseFailure = 10; |
|
} |
|
if (!IsErrorPage() && localStorage.getItem("errorPageCount")) { |
|
localStorage.removeItem("errorPageCount"); |
|
} |
|
|
|
if (url === "https://www.blaseball.com/") { |
|
ReadResults(); |
|
} |
|
|
|
// This exists on all pages, so can always run it safely |
|
ReadToastResults(); |
|
|
|
if (automaticTeamParsingEnabled) { |
|
AutomateTeamStarParsing(); |
|
} |
|
|
|
Loop_CheckAutobid(); |
|
|
|
catchCountdownFinish(); |
|
|
|
ReportCoinBalance(); |
|
|
|
trackGameResults(); |
|
|
|
console.log(`DEBUG: mainLoop: ${DeterminePageState()}`); |
|
} |
|
|
|
// ########## Main Loop Helpers ########## // |
|
function IsErrorPage() { |
|
//return document.title === "502 Bad Gateway"; |
|
return DeterminePageState() === 'ERROR'; |
|
} |
|
function DeterminePageState() { |
|
let userNav = document.querySelector('.Navigation-User'); |
|
if (userNav && userNav.children.length === 2 |
|
&& userNav.children[0].innerText === "SIGNUP" |
|
&& userNav.children[1].innerText === "LOGIN") { |
|
return "LOGIN"; |
|
} |
|
|
|
let modalsExists = document.querySelectorAll('.Modal--Generic'); |
|
if (modalsExists.length > 0) { |
|
let allModalClasses = [...modalsExists].map(m => m.classList); |
|
let selectedAll = allModalClasses.reduce((acc, cur) => acc.concat([...cur]), []); |
|
return ['MODAL', selectedAll.filter(x => x.indexOf('Modal') < 0)].join(':'); |
|
} |
|
|
|
let currentNavStates = document.querySelectorAll('.Navigation-Button-Current'); |
|
|
|
if (currentNavStates.length >= 2) { |
|
return [...currentNavStates].map(x => x.innerText).join(':') |
|
|
|
// TODO?: Check if any '.GameWidget-Status--Live' exist, and decide between 'LEAGUE:WATCH LIVE' or 'LEAGUE:RESULTS' |
|
} |
|
else if (currentNavStates.length === 1) { |
|
return currentNavStates[0].innerText; |
|
} |
|
else if (document.querySelector('.Stubs-Header') && document.querySelector('.Stubs-Header').innerText === "The Season is Over!") { |
|
// TODO is this really a state |
|
return "SEASON OVER"; |
|
} |
|
else if (document.title.indexOf('Maintenance') >= 0) { |
|
return "MAINTENANCE"; |
|
} |
|
else { |
|
// No Nav, wtf is this state?? |
|
return "ERROR"; |
|
} |
|
} |
|
// This state machine is meant to be able to recover from unknown states, and get to known states |
|
// i.e. if we're on a page we can't bid on, but auto-bidding is enabled, this could get to a page we can bet on |
|
function CreateStateMachine() { |
|
var fsm = new StateMachine({ |
|
transitions: [ |
|
{ name: 'logout', from: '*', to: 'LOGIN' }, |
|
{ name: 'gotoUpcoming', from: '*', to: 'LEAGUE:PLACE BETS' }, |
|
{ name: 'gotoStandings', from: '*', to: 'LEAGUE:STANDINGS' }, |
|
{ name: 'gotoResults', from: '*', to: 'LEAGUE:WATCH LIVE' }, |
|
{ name: 'goto', from: '*', to: function(s) { return s } }, |
|
], |
|
methods: { |
|
onTransition: function(lifecycle, arg1, arg2) { |
|
console.log('TRANSITION', lifecycle.transition, lifecycle.from, lifecycle.to); |
|
}, |
|
onGotoUpcoming: function() { |
|
console.log('STATE: moving page to /upcoming'); |
|
var upcomingButtons = document.querySelectorAll('a[href="/upcoming"]'); |
|
if (upcomingButtons.length >= 1) { |
|
console.log('DEBUG: Clicking on', upcomingButtons[0]); |
|
upcomingButtons[0].click(); |
|
|
|
return true; |
|
} |
|
else { |
|
console.log('WARNING: No /upcoming links found, forcing redirect'); |
|
slackLogCollection('WARNING', 'No /upcoming links found, forcing redirect'); |
|
document.location = '/upcoming'; |
|
|
|
return false; |
|
} |
|
}, |
|
} |
|
}); |
|
|
|
fsm.goto(DeterminePageState()); |
|
|
|
return fsm; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ########## Control Panel and Handlers ########## // |
|
|
|
let controlPanelInjected = false; |
|
function InjectControlPanel() { |
|
if (controlPanelInjected) { |
|
return; |
|
} |
|
|
|
console.log('DEBUG: Injecting control panel!'); |
|
|
|
const controlPanel = document.createElement('div'); |
|
controlPanel.className = 'ControlPanel'; |
|
controlPanel.id = 'ControlPanelId'; |
|
|
|
controlPanel.innerHTML = ` |
|
Helper Panel |
|
<div id='teamUpdateToggle' class='Button'>Auto-Team Updates: On</div> |
|
<div id='refreshTeamData' class='Button'>Refresh Team Data</div> |
|
<br/> |
|
<div id='autoBidToggle' class='Button'>Auto-bid setup error</div> |
|
<div id='autoBidTriggerTime' class='Button' title='Set this to -1 to disable autobid'>Auto-bid at Mins left <input type='number' value='15' /></div> |
|
<div id='autoBidAmount' class='Button'>Bid Amount <input type='number' placeholder='Max' /></div> |
|
<div id='autoBidManual' class='Button'>Auto-Bid Now {TESTME}</div> |
|
|
|
<div id='stats'></div> |
|
`; |
|
|
|
document.body.appendChild(controlPanel); |
|
|
|
|
|
controlPanel.querySelector('#teamUpdateToggle') |
|
.addEventListener("click", ToggleAutoTeamUpdate, false); |
|
controlPanel.querySelector( '#refreshTeamData') |
|
.addEventListener("click", TriggerRefreshTeamData, false); |
|
controlPanel.querySelector('#autoBidToggle') |
|
.addEventListener("click", ToggleAutoBid, false); |
|
// controlPanel.querySelector('#autoBidManual') |
|
// .addEventListener("click", StepAutoBidTriggered, false); |
|
controlPanel.querySelector('#autoBidManual') |
|
.addEventListener("click", trackGameResults, false); |
|
|
|
GM_addStyle(` |
|
.ControlPanel { |
|
background-color: #aaa; |
|
position: fixed; |
|
text-align: center; |
|
top: 250px; |
|
padding: 8px; |
|
border-radius: .6rem; |
|
font-size: .8rem; |
|
} |
|
.Button { |
|
border: #666 solid 1px; |
|
background-color: #ddd; |
|
padding: .3rem; |
|
border-radius: .3rem; |
|
cursor: pointer; |
|
} |
|
.Button > input { |
|
background-color: white; |
|
border: solid 1px; |
|
width: 2rem; |
|
} |
|
`); |
|
|
|
// Setup: |
|
ToggleAutoBid(); |
|
|
|
controlPanelInjected = true; |
|
|
|
// document.body.insertAdjacentHTML('beforeend', ` |
|
// <div class='ControlPanel'> |
|
// Helper Panel |
|
|
|
// </div>`); |
|
} |
|
function changeToggleLabel(e, state) { |
|
const text = e.target.innerText; |
|
e.target.innerText = text.replace(/(On|Off)$/, '') + (state ? 'On' : 'Off'); |
|
} |
|
function ToggleAutoTeamUpdate(e) { |
|
automaticTeamParsingEnabled = automaticTeamParsingEnabled === false; |
|
|
|
changeToggleLabel(e, automaticTeamParsingEnabled); |
|
} |
|
let automaticTeamParsingNewerThan = undefined; |
|
function TriggerRefreshTeamData(e) { |
|
automaticTeamParsingNewerThan = new Date(); |
|
} |
|
let autoBidStrategyIndex = 0; |
|
let autoBidStrategyFunction = undefined; |
|
function ToggleAutoBid(e) { |
|
var strategies = [ |
|
//{ Name: "Off", SelectTeamIndexToBetOn: undefined }, |
|
{ Name: "Star Rating", SelectTeamIndexToBetOn: SelectTeamIndexToBetOn_StarRating }, |
|
]; |
|
|
|
|
|
if (e) { |
|
++autoBidStrategyIndex; |
|
autoBidStrategyIndex %= strategies.length; |
|
} |
|
|
|
|
|
let strat = strategies[autoBidStrategyIndex]; |
|
autoBidStrategyFunction = strat.SelectTeamIndexToBetOn; |
|
var autoBidButton = document.querySelector('#autoBidToggle'); |
|
autoBidButton.innerText = `Auto-Bid: ${strat.Name}`; |
|
} |
|
|
|
// ########## Auto-bidding Functions (LEGACY-TODO replace this with the new logic below) ########## // |
|
|
|
let autoBidTimeHit = false; |
|
let leftUpcomingPageTime = undefined; |
|
function Loop_CheckAutobid() { |
|
const triggerTime = parseInt(document.querySelector('#autoBidTriggerTime > input').value); |
|
|
|
if (triggerTime >= 0) { |
|
// Auto-bid trigger time is configured, have logic which supports it |
|
|
|
const onBiddingPage = document.URL === "https://www.blaseball.com/upcoming"; |
|
|
|
// TODO do this logic in a more condensed spot?? |
|
// Either ready to bid, or in the process of bidding: |
|
if (onBiddingPage || document.querySelector('.Bet-Form')) { |
|
// console.log('DEBUG: On betting page, reset redirect time'); |
|
leftUpcomingPageTime = undefined; |
|
} |
|
else if (leftUpcomingPageTime === undefined) { |
|
console.log(`INFO: Setting time to redirect to /upcoming: was on ${document.URL}`); |
|
leftUpcomingPageTime = new Date(); |
|
} |
|
else if ((new Date() - leftUpcomingPageTime) >= (5 * 60 * 1000)) { |
|
// Redirect to placing bids screen in case of errors or shit, if not on this page for more than 50 seconds |
|
// Any manual thing would likely be done by 50 seconds anyway??? |
|
console.log('WARNING: Been too long on a different page, redirecting to upcoming page!!'); |
|
slackLogCollection('WARNING', 'Been too long on a different page, redirecting to upcoming page'); |
|
document.location = '/upcoming'; |
|
} |
|
else if (leftUpcomingPageTime) { |
|
//console.log(`DEBUG: on ${document.URL} which is not main page since ${leftUpcomingPageTime}`); |
|
} |
|
|
|
|
|
if (onBiddingPage) { |
|
const countdownElement = document.querySelector('.Countdown'); |
|
const countdownValue = countdownElement && countdownElement.textContent; |
|
|
|
if (countdownValue && countdownValue.indexOf(`0Hours${triggerTime}Minutes`) === 0) { |
|
if (!autoBidTimeHit) { |
|
console.log('INFO: ### Time to Auto-bid!!!!! ###', countdownValue); |
|
slackLogCollection('INFO', `Time to Auto-bid! Countdown value: ${countdownValue}`); |
|
|
|
autoBidErrorCount = 0; |
|
AutoBidAll(); |
|
|
|
autoBidTimeHit = true; |
|
} |
|
} |
|
else { |
|
autoBidTimeHit = false; |
|
} |
|
} |
|
} |
|
} |
|
let autoBidErrorCount = 0; |
|
function AutoBidAll() { |
|
let result = AutoBid(); |
|
|
|
if (result) { |
|
setTimeout(AutoBidAll, 1500); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ########## Auto-bidding Functions (new) ########## // |
|
|
|
function StepAutoBidTriggered(e) { |
|
let result = StepAutoBid(); |
|
console.log('StepAutoBid', result); |
|
} |
|
|
|
|
|
let autoBidStateMachine_NEW = null; |
|
function StepAutoBid() { |
|
|
|
|
|
// //TESTING |
|
// var fsm = new StateMachine({ |
|
// init: 'solid', |
|
// transitions: [ |
|
// { name: 'melt', from: 'solid', to: 'liquid' }, |
|
// { name: 'freeze', from: 'liquid', to: 'solid' }, |
|
// //{ name: 'vaporize', from: 'liquid', to: function() { console.log('OMG Im a LIQUID'); return 'gas'; } }, |
|
// { name: 'vaporize', from: 'liquid', to: 'gas' }, |
|
// { name: 'condense', from: 'gas', to: 'liquid' }, |
|
// { name: 'reset', from: '*', to: 'READY' }, |
|
// ], |
|
// data: function () { |
|
// return { |
|
// formOpenTime: null, |
|
// submitTryCount: 0 |
|
// }; |
|
// }, |
|
// methods: { |
|
// onMelt: function() { console.log('I melted') }, |
|
// onLeaveSolid: function () { |
|
// this.formOpenTime = new Date(); |
|
// }, |
|
// onFreeze: function() { console.log('I froze') }, |
|
// onLiquid: function() { console.log(`OMG Im a LIQUID: ${this.formOpenTime}`); }, |
|
// onBeforeVaporize: function() { console.log('I vaporized'); return false; }, |
|
// onCondense: function() { console.log('I condensed') } |
|
// } |
|
// }); |
|
// console.log(fsm.state); |
|
// fsm.melt(); |
|
// console.log(fsm.state); |
|
// fsm.vaporize(); |
|
// console.log(fsm.state); |
|
// fsm.vaporize(); |
|
// console.log(fsm.state); |
|
// fsm.freeze(); |
|
// console.log(fsm.state); |
|
// console.log(fsm.formOpenTime); |
|
|
|
|
|
// return; |
|
|
|
|
|
|
|
if (autoBidStateMachine_NEW === null) { |
|
autoBidStateMachine_NEW = CreateAutoBidStateMachine(); |
|
} |
|
|
|
var sm = autoBidStateMachine_NEW; |
|
|
|
console.log(`INFO: Current state: ${sm.state} (attempts: ${sm.transitionAttempts}, submit: ${sm.submitTryCount})`); |
|
|
|
|
|
if (sm.transitionAttempts > 3) { |
|
console.log(`ERROR: Too many transition attempts: ${sm.transitionAttempts}`); |
|
sm.reset(); |
|
} |
|
switch (sm.state) { |
|
case 'READY': |
|
return sm.openBetForm(); |
|
case 'BET-FORM-TEAM': |
|
sm.selectTeam(); |
|
return; |
|
case 'BET-FORM-SUBMIT': |
|
if (!sm.submitBet() || sm.submitTryCount >= 5) { |
|
console.log(`ERROR: Too many tries: ${sm.submitTryCount}`); |
|
sm.reset(); |
|
} |
|
else if (sm.submitTryCount >= 1){ |
|
console.log(`DEBUG: Needing to retry submitBet(): ${sm.submitTryCount}`); |
|
} |
|
return; |
|
case 'BET-FORM-DONE': |
|
sm.veritySuccess(); |
|
return; |
|
default: |
|
console.log('Wtf unknown state?'); |
|
debugger; |
|
} |
|
} |
|
function CreateAutoBidStateMachine() { |
|
var fsm = new StateMachine({ |
|
init: 'READY', |
|
transitions: [ |
|
{ name: 'openBetForm', from: 'READY', to: 'BET-FORM-TEAM' }, |
|
{ name: 'selectTeam', from: 'BET-FORM-TEAM', to: 'BET-FORM-SUBMIT' }, |
|
{ name: 'submitBet', from: 'BET-FORM-SUBMIT', to: 'BET-FORM-DONE' }, |
|
{ name: 'veritySuccess', from: 'BET-FORM-DONE', to: function() { return !document.querySelector('.Bet-Form') ? 'READY' : 'BET-FORM-SUBMIT'; } }, |
|
{ name: 'reset', from: '*', to: 'READY' }, |
|
], |
|
data: function() { |
|
return { |
|
formOpenTime: null, |
|
submitTryCount: 0, |
|
transitionAttempts: 0 |
|
}; |
|
}, |
|
methods: { |
|
onTransition: function(lifecycle, arg1, arg2) { |
|
this.transitionAttempts++; |
|
console.log(`TRANSITION: ${lifecycle.transition} :: ${lifecycle.from} => ${lifecycle.to}`); |
|
}, |
|
onLeaveReady: function() { |
|
// Find bet button |
|
console.log('INFO: Auto-bidding, finding a game to bet on...') |
|
const bettingButtons = document.querySelectorAll('.GameWidget-Upcoming-BetButtons a'); |
|
|
|
// Press the button |
|
if (bettingButtons.length > 0) { |
|
bettingButtons[0].click(); |
|
|
|
this.formOpenTime = new Date(); |
|
this.submitTryCount = 0; |
|
this.transitionAttempts = 0; |
|
|
|
return true; |
|
} |
|
else { |
|
console.log('INFO: No button found, stay in READY') |
|
return false; |
|
} |
|
}, |
|
onOpenBetForm: function(lifecycle) { |
|
console.log('onOpenBetForm: ' + lifecycle.to); |
|
|
|
if (document.querySelector('.Bet-Form')) { |
|
return true; |
|
} |
|
else { |
|
console.log('No Bet-Form found, at this time. Returning false'); |
|
return false; |
|
} |
|
}, |
|
onSelectTeam: function() { |
|
return betFormSelectTeam(); |
|
}, |
|
onSubmitBet: function() { |
|
console.log('DEBUG: pressing submit button'); |
|
// TODO kind of want this to run on each attempt to go to verify success?? Is there a way to do that? |
|
let submitButton = document.querySelector('.Bet-Submit'); |
|
if (submitButton) { |
|
submitButton.click(); |
|
return true; |
|
} |
|
else { |
|
console.log('WARNING: No bet submit button found'); |
|
return false; |
|
} |
|
}, |
|
onVerifySuccess: function() { |
|
console.log(`DEBUG: incrementing submitTryCount, currently at: ${this.submitTryCount}`); |
|
this.submitTryCount++; |
|
}, |
|
onReset: function() { |
|
this.transitionAttempts = 0; |
|
|
|
var closeButton = document.querySelector('.Modal-Close'); |
|
if (closeButton) { |
|
console.log("DEBUG: Found close button, pushing now"); |
|
closeButton.click(); |
|
} |
|
else { |
|
console.log("DEBUG: No close button found"); |
|
} |
|
}, |
|
} |
|
}); |
|
|
|
return fsm; |
|
} |
|
function betFormSelectTeam() { |
|
// Verify bet form is up |
|
if (!document.querySelector('.Bet-Form')) { |
|
console.log('ERROR: Was expecting .Bet-Form to exist at this state'); |
|
return false; |
|
} |
|
|
|
// Select team |
|
var teamElements = document.querySelectorAll('.Bet-Form-Team-Name'); |
|
var percentTeam1Win = document.querySelector('.Bet-Form-Team-Percentage').textContent; |
|
//TODO include bet id?? Is that the game id?? |
|
|
|
|
|
var teamId1 = GetTeamIdByName(teamElements[0].textContent); |
|
var teamId2 = GetTeamIdByName(teamElements[1].textContent); |
|
|
|
var team1Data = GetTeam(teamId1); |
|
var team2Data = GetTeam(teamId2); |
|
|
|
var countdownElement = document.querySelector('.Countdown'); |
|
console.log('DEBUG: Countdown', countdownElement && countdownElement.textContent); |
|
console.log('DEBUG: Betting', team1Data, team2Data, percentTeam1Win); |
|
|
|
|
|
var teamIndex = autoBidStrategyFunction(team1Data, team2Data, percentTeam1Win); |
|
console.log(`DEBUG: Betting on: ${teamIndex}`); |
|
teamElements[teamIndex].click(); |
|
|
|
|
|
|
|
|
|
var maxBet = parseInt(/\d+$/.exec(document.querySelector('.Bet-Form-Inputs-Amount-MaxBet').textContent)); |
|
var betAmount = CalculateBet(maxBet, team1Data, team2Data); |
|
console.log(`DEBUG: Betting amount: ${betAmount}`); |
|
|
|
setNativeValue(document.getElementById('amount'), betAmount); |
|
|
|
slackDataCollection({ |
|
type: 'bet-placed', |
|
strategy: 'teamStarRating', |
|
team1: team1Data, |
|
team2: team2Data, |
|
team1PercentWin: percentTeam1Win, |
|
bettingOnIndex: teamIndex, |
|
betAmount: betAmount, |
|
}); |
|
|
|
return true; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
// ########## Auto-bidding More Functions?? (LEGACY) ########## // |
|
|
|
// #region: legacy auto-bid |
|
//TODO add a wait-cycle step, seems like its rate limited to around 5 seconds... |
|
let autoBidStateMachine = undefined; |
|
const autoBidState_SelectTeam = "BetScreenOpen"; |
|
const autoBidState_Submitted = "BetScreenSubmitted"; |
|
function AutoBid(e) { |
|
|
|
if (!document.querySelector('.Bet-Form')) { |
|
// Bet form not found, reset |
|
autoBidStateMachine = undefined; |
|
} |
|
|
|
if (!autoBidStateMachine) { |
|
// Open Bet screen |
|
console.log('INFO: Auto-bidding, finding a game to bet on...') |
|
const bettingButtons = document.querySelectorAll('.GameWidget-Upcoming-BetButtons a'); |
|
|
|
if (bettingButtons.length > 0) { |
|
bettingButtons[0].click(); |
|
|
|
autoBidStateMachine = autoBidState_SelectTeam; |
|
if (e) { |
|
console.log("e EXISTS #################"); |
|
// TODO testing if removing this would make it not get caught by the rate limiting |
|
setTimeout(AutoBid, 500); |
|
} |
|
} |
|
else { |
|
console.log("INFO: Didn't find a Bet button, ending auto-bid"); |
|
autoBidStateMachine = undefined; |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
else if (autoBidStateMachine == autoBidState_SelectTeam) { |
|
// Select team |
|
var teamElements = document.querySelectorAll('.Bet-Form-Team-Name'); |
|
var percentTeam1Win = document.querySelector('.Bet-Form-Team-Percentage').textContent; |
|
|
|
|
|
var teamId1 = GetTeamIdByName(teamElements[0].textContent); |
|
var teamId2 = GetTeamIdByName(teamElements[1].textContent); |
|
|
|
var team1Data = GetTeam(teamId1); |
|
var team2Data = GetTeam(teamId2); |
|
|
|
var countdownElement = document.querySelector('.Countdown'); |
|
console.log('DEBUG: Countdown', countdownElement && countdownElement.textContent); |
|
console.log('DEBUG: Betting', team1Data, team2Data, percentTeam1Win); |
|
|
|
|
|
var teamIndex = autoBidStrategyFunction(team1Data, team2Data, percentTeam1Win); |
|
console.log(`DEBUG: Betting on: ${teamIndex}`); |
|
teamElements[teamIndex].click(); |
|
|
|
|
|
|
|
|
|
var maxBet = parseInt(/\d+$/.exec(document.querySelector('.Bet-Form-Inputs-Amount-MaxBet').textContent)); |
|
var betAmount = CalculateBet(maxBet, team1Data, team2Data); |
|
console.log(`DEBUG: Betting amount: ${betAmount}`); |
|
|
|
|
|
setNativeValue(document.getElementById('amount'), betAmount); |
|
|
|
slackDataCollection({ |
|
type: 'bet-placed', |
|
strategy: 'teamStarRating-legacyAutoBid', |
|
team1: team1Data, |
|
team2: team2Data, |
|
team1PercentWin: percentTeam1Win, |
|
bettingOnIndex: teamIndex, |
|
betAmount: betAmount, |
|
}); |
|
|
|
|
|
document.querySelector('.Bet-Submit').click(); |
|
|
|
autoBidStateMachine = autoBidState_Submitted; |
|
|
|
return true; |
|
} |
|
else if (autoBidStateMachine == autoBidState_Submitted) { |
|
console.log('ERROR: Seems like the bid screen is being slow or had an error, closing it...'); |
|
++autoBidErrorCount; |
|
|
|
var closeButton = document.querySelector('.Modal-Close'); |
|
if (closeButton) { |
|
console.log("DEBUG: Found close button, pushing now"); |
|
closeButton.click(); |
|
|
|
if (autoBidErrorCount <= 5) { |
|
// return true, as this is a valid process to recover from calling this method more |
|
return true; |
|
} |
|
else { |
|
console.log("WARNING: auto-bid error count reached too much"); |
|
return false; |
|
} |
|
} |
|
else { |
|
console.log("DEBUG: No close button found"); |
|
} |
|
// no need for state change, the lack of modal will trigger a reset |
|
} |
|
|
|
return false; |
|
} |
|
// #endregion: legacy auto-bid |
|
|
|
// ########## Bidding Helper Functions ########## // |
|
|
|
function SelectTeamIndexToBetOn_StarRating(team1Data, team2Data, percentTeam1Win) { |
|
// If its within a certain range, don't use the stars |
|
var starDifference = Math.abs(team1Data.TeamStars - team2Data.TeamStars); |
|
|
|
if (starDifference >= 1) { |
|
console.log(`DEBUG: By Stars: ${team1Data.TeamName}:${team1Data.TeamStars} vs ${team2Data.TeamName}:${team2Data.TeamStars}`); |
|
return team1Data.TeamStars > team2Data.TeamStars |
|
? 0 |
|
: 1; |
|
} |
|
else { |
|
// When stars are equalish, do the reverse of the percentage cause fun?? |
|
console.log(`DEBUG: Reverse percentage when stars are close: ${percentTeam1Win} win for ${team1Data.TeamName}:${team1Data.TeamStars} vs ${team2Data.TeamName}:${team2Data.TeamStars}`); |
|
return parseFloat(percentTeam1Win) > 50 ? 1 : 0; |
|
} |
|
} |
|
function CalculateBet(maxBet, team1Data, team2Data) { |
|
// TODO implement some logic here |
|
|
|
const configuredBidAmount = document.querySelector('#autoBidAmount > input').value; |
|
|
|
if (configuredBidAmount) { |
|
const bidAmount = parseInt(configuredBidAmount); |
|
return bidAmount; |
|
} |
|
|
|
return maxBet; |
|
} |
|
|
|
|
|
let teamNameLookup = null; |
|
function GetTeamIdByName(teamName) { |
|
// Rebuild if it doesn't exist, or if the requested name isn't in it (optimistically outdated data) |
|
if (!teamNameLookup || !teamNameLookup.has(teamName)) { |
|
var data = [...document.querySelectorAll('.GameWidget-ScoreLine')] |
|
.map(x => { |
|
return { |
|
TeamId: x.href.substring('https://www.blaseball.com/team/'.length), |
|
TeamName: x.querySelector('.GameWidget-ScoreName').innerText |
|
}; |
|
}); |
|
|
|
console.log(data); |
|
|
|
teamNameLookup = new Map(data.map(x => [x.TeamName, x.TeamId])); |
|
} |
|
|
|
return teamNameLookup.get(teamName); |
|
} |
|
|
|
|
|
|
|
// ########## Parse and Display Star Ratings For Teams ########## // |
|
|
|
let teamParseFailure = 0; |
|
let allTeamDataParsedDate = null; |
|
let automaticUpdateTeamId = null; |
|
function AutomateTeamStarParsing() { |
|
|
|
// Need this to be a quite small amount of time, because the data gets redrawn and data shifts around (especially when betting) |
|
if (allTeamDataParsedDate |
|
&& (new Date() - allTeamDataParsedDate) < 2 * 1000) { |
|
return; |
|
} |
|
// console.log("AutomateTeamStarParsing"); |
|
|
|
let teamElements = [...document.getElementsByClassName('GameWidget-ScoreLine')]; |
|
const maximumTimeToLive = 1 * 60 * 60 * 1000; // 1 hour |
|
|
|
for (let i = 0; i < teamElements.length; ++i) { |
|
var teamElement = teamElements[i]; |
|
var teamId = teamElement.href.substring('https://www.blaseball.com/team/'.length); |
|
var teamData = GetTeam(teamId); |
|
|
|
var teamDataNeedsToBeLoaded = teamData === null || (new Date() - new Date(teamData.Date)) > maximumTimeToLive; |
|
var teamDataMarkedOutOfDate = automaticTeamParsingNewerThan && automaticTeamParsingNewerThan > new Date(teamData.Date); |
|
if (teamDataNeedsToBeLoaded || teamDataMarkedOutOfDate) { |
|
automaticUpdateTeamId = teamId; |
|
teamElement.click(); |
|
return; |
|
} |
|
|
|
// Inject the data into the page |
|
var correctDataInjected = !teamElement.dataset.displayApplied || teamElement.dataset.displayApplied !== teamElement.href; |
|
if (correctDataInjected) { |
|
teamElement.dataset.displayApplied = teamElement.href; |
|
//console.log("applying modifications"); |
|
let parentElement = teamElement.querySelector(".GameWidget-ScoreTeamInfo"); |
|
|
|
let displaySpan = parentElement.querySelector(".StarStats"); |
|
|
|
if (!displaySpan) { |
|
displaySpan = document.createElement('span'); |
|
displaySpan.className = 'StarStats'; |
|
|
|
displaySpan.style = "font-size: 14px; color: #ccc; margin-left: .2em; font-family: 'Lora','Courier New',monospace,serif;"; |
|
|
|
parentElement.appendChild(displaySpan); |
|
} |
|
|
|
//TODO <span title='test'> |
|
displaySpan.textContent = `${teamData.TeamStars}⭐\n${teamData.Mean.toFixed(2)}\n${teamData.Median.toFixed(2)}\n${(teamData.SquaredStars / teamData.PlayerCount).toFixed(2)}`; |
|
|
|
console.log(`DEBUG: Updated ${teamId}`); |
|
} |
|
} |
|
|
|
// IF GET TO HERE, ALL DATA IS INJECTED (for now) |
|
if (teamElements.length > 0) { |
|
allTeamDataParsedDate = new Date(); |
|
teamParseFailure = 0; |
|
} |
|
else { |
|
++teamParseFailure; |
|
if (teamParseFailure > 5) { |
|
allTeamDataParsedDate = new Date(); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
// ########## Read the win/loss notifications to see results ########## // |
|
|
|
function ReadToastResults() { |
|
// Element with the text: |
|
//react-toast-notifications__toast react-toast-notifications__toast--info css-ldacev |
|
const toasts = [...document.getElementsByClassName('react-toast-notifications__toast')]; |
|
|
|
const winColor = "#0C0"; |
|
const lossColor = "#C00"; |
|
const passiveIncomeColor = "#F90"; |
|
const unknownEventColor = "#F0F"; |
|
|
|
// Set background-color on element: react-toast-notifications__toast__icon-wrapper css-pbi326 |
|
toasts.forEach((t) => { |
|
let colorElement = t.querySelector(".react-toast-notifications__toast__icon-wrapper"); |
|
|
|
let message = t.textContent; |
|
if (message.indexOf("won") >= 0) { |
|
colorElement.style.backgroundColor = winColor; |
|
} |
|
else if (message.indexOf("lost") >= 0) { |
|
colorElement.style.backgroundColor = lossColor; |
|
} |
|
else if (message.indexOf("earned") >= 0) { |
|
colorElement.style.backgroundColor = passiveIncomeColor; |
|
} |
|
else { |
|
colorElement.style.backgroundColor = unknownEventColor; |
|
} |
|
}); |
|
|
|
|
|
|
|
// Calculate results |
|
const messages = toasts.map(x => x.innerText); |
|
const [gainLoss, totalStake, totalWinnings, count, winCount] = ParseGainLoss(messages); |
|
|
|
if (count) { |
|
console.log(`Toast Notification- Gain/Loss: ${gainLoss} (Staked: ${totalStake}, Winnings: ${totalWinnings}, Return: ${(gainLoss*100/totalStake).toFixed(2)}%) over ${count} bets`); |
|
} |
|
|
|
|
|
|
|
// Data collection |
|
var filteredMessages = messages.filter(x => x !== "Bet Placed\nClose"); |
|
dataCollectionUnique('toast-notification', filteredMessages); |
|
} |
|
|
|
|
|
// ########## Parsing Team Data (mostly for star ratings) ########## // |
|
|
|
let currentPage = ""; |
|
let loadStart = undefined; |
|
|
|
function ParseTeamDataScreen() { |
|
if (currentPage != document.URL) { |
|
currentPage = document.URL; |
|
loadStart = new Date(); |
|
|
|
// TODO this is a pretty hacky solution, intention is to avoid console messages of: |
|
// "INFO: Setting time to redirect to /upcoming: was on https://www.blaseball.com/team/8d87c468-699a-47a8-b40d-cfb73a5660ad" |
|
// Whenever the team data is loaded, so just manually setting that property when doing this |
|
// BUT also want to retain the value if there is one |
|
leftUpcomingPageTime = leftUpcomingPageTime || new Date(); |
|
} |
|
|
|
let teamId = document.URL.substring('https://www.blaseball.com/team/'.length); |
|
|
|
let teamName = document.querySelector('.Team-Name'); |
|
let playerLines = document.getElementsByClassName('Team-Player-Line'); |
|
|
|
var playerData = [...playerLines].map(x => |
|
{ |
|
let playerHeader = x.getElementsByClassName('Team-Player-Header'); |
|
let playerShelled = (playerHeader[0].className.indexOf('Team-Player-Shelled') >= 0) |
|
|
|
return { |
|
PlayerId: x.href.substring('https://www.blaseball.com/player/'.length), |
|
PlayerName: playerHeader.innerText, |
|
Vibe: x.querySelector('.Team-Player-Vibe').className, |
|
Shelled: playerShelled, |
|
Rating: CountStars([...x.querySelectorAll('.Team-Player-Ratings > span > svg')].map(x => x.innerHTML.length)) |
|
}; |
|
}); |
|
|
|
// TODO store this player data somewhere too |
|
//console.log(playerData); |
|
//var teamStars = playerData.reduce((accumulator, currentValue) => accumulator + currentValue.Rating, 0); |
|
//console.log(teamName.innerText, "Team stars:", teamStars); |
|
|
|
if (playerData.length > 0) { |
|
// Team data successfully loaded |
|
|
|
// Exclude shelled players?????? |
|
let shelledPlayers = playerData.filter(x => x.Shelled); |
|
playerData = playerData.filter(x => !x.Shelled); |
|
|
|
playerData.sort((a, b) => a.Rating - b.Rating); |
|
const mid = Math.ceil(playerData.length / 2); |
|
const median = playerData.length % 2 == 0 ? (playerData[mid].Rating + playerData[mid - 1].Rating) / 2 : playerData[mid - 1].Rating; |
|
const initialValues = { |
|
TeamName: teamName.innerText, |
|
Date: new Date().toISOString(), |
|
PlayerCount: playerData.length, |
|
TeamStars: 0, |
|
ShelledPlayers: shelledPlayers.length, |
|
ShelledStars: shelledPlayers.reduce((acc, cur) => acc + cur.Rating, 0), |
|
Mean: 0, |
|
Median: median, |
|
SquaredStars: 0, |
|
}; |
|
const output = playerData.reduce((acc, currentValue) => { |
|
acc.TeamStars += currentValue.Rating; |
|
acc.Mean = acc.TeamStars / playerData.length; |
|
acc.SquaredStars += currentValue.Rating * currentValue.Rating; |
|
return acc; |
|
}, initialValues); |
|
console.log(output); |
|
|
|
// Check data consistency |
|
var previousData = GetTeam(teamId); |
|
if (previousData === null |
|
|| previousData.TeamStars != output.TeamStars |
|
|| previousData.Mean != output.Mean |
|
|| previousData.Median != output.Median |
|
|| previousData.ShelledStars != output.ShelledStars) { |
|
|
|
console.log("WARNING: OMG DATA CHANGED:", previousData, output); |
|
slackDataCollection({type: 'team-data-changed', old: previousData, new: output}); |
|
//debugger; |
|
} |
|
else { |
|
console.log('DEBUG: Team data updated, but no changes detected'); |
|
} |
|
|
|
// Store the successful calculation |
|
SetTeam(teamId, output); |
|
|
|
if (teamId === automaticUpdateTeamId) { |
|
CloseAndResetTeamModal(); |
|
} |
|
} |
|
else if (teamId === automaticUpdateTeamId && (new Date() - loadStart) > 5000) { |
|
// Consider the load timed out, and reset |
|
console.log('DEBUG: Exceeded a 5 second timeout, closing and retrying...'); |
|
CloseAndResetTeamModal(); |
|
} |
|
} |
|
function CloseAndResetTeamModal() { |
|
currentPage = ""; |
|
loadStart = undefined; |
|
automaticUpdateTeamId = null; |
|
document.getElementsByClassName('Modal-Close')[0].click(); |
|
} |
|
|
|
|
|
// ########## Team Data Datastorage ########## // |
|
|
|
function GetTeam(teamId) { |
|
return JSON.parse(localStorage.getItem("team:" + teamId)); |
|
} |
|
function SetTeam(teamId, data) { |
|
localStorage.setItem("team:" + teamId, JSON.stringify(data)); |
|
} |
|
|
|
function CountStars(lengthArray) { |
|
var oneStar = 474; |
|
var halfStar = 205; |
|
|
|
var output = 0; |
|
|
|
var first = lengthArray.pop(); |
|
if (first === undefined) { |
|
return 0; |
|
} |
|
if (first === halfStar) { |
|
output += 0.5; |
|
} |
|
else if (first === oneStar) { |
|
output += 1; |
|
} |
|
else { |
|
console.log("ERROR, final star didn't match:", first); |
|
} |
|
|
|
// TODO make sure all the rest match oneStar, else error! |
|
|
|
output += lengthArray.length; |
|
return output; |
|
} |
|
|
|
|
|
|
|
// ########## Parsing Game Data From Widget (NEWER-TODO replace any manual parsing of this data) ########## // |
|
|
|
function trackGameResults() { |
|
const gameElements = document.querySelectorAll('.GameWidget'); |
|
|
|
const gameResults = parseGameResults(gameElements); |
|
dataCollectionUnique('game-results', gameResults); |
|
} |
|
// Pass in array of document.querySelectorAll('.GameWidget'); |
|
function parseGameResults(gameWidgets) { |
|
const gameWidgetArray = [...gameWidgets]; |
|
|
|
const gameData = gameWidgetArray.map( |
|
x => { |
|
let teamData = x.querySelectorAll('.GameWidget-ScoreBacking > a'); |
|
let upcomingBetData = x.querySelectorAll('.GameWidget-Upcoming-Bets'); |
|
let output = { |
|
gameStatus: x.getText('.GameWidget-Status'), |
|
// final results only have a '.WeatherIcon' with no text |
|
gameLabel: x.getText('.GameWidget-ScoreLabel'), |
|
teams: [...parseGameResultsTeamData(teamData, upcomingBetData)], |
|
}; |
|
|
|
return output; |
|
}); |
|
return gameData; |
|
} |
|
function parseGameResultsTeamData(teams, bettingElement) { |
|
if (teams.length !== 2) { |
|
console.log('ERROR: Should have found the two teams of the game, but apparently not'); |
|
debugger; |
|
} |
|
if (bettingElement.length !== 0 && bettingElement.length !== 1) { |
|
console.log('ERROR: Should have found 1 elements in betData, if it is passed in'); |
|
debugger; |
|
} |
|
|
|
//x.querySelectorAll('.GameWidget-UpcomingBet') == " 100 on New York Millennials" |
|
|
|
let upcomingOddsLookup = {}; |
|
if (bettingElement.length === 1) { |
|
// This should have 2 teams in it: |
|
let upcomingTeamOdds = bettingElement[0].querySelectorAll('.GameWidget-Upcoming-OddsTeam'); |
|
if (upcomingTeamOdds.length > 0) { |
|
// TODO build a lookup of some sort for teamname to percentage |
|
let oddsByTeam = upcomingTeamOdds.map(x => { |
|
x.getText('.GameWidget-Upcoming-Favorites-Team'); |
|
x.getText('.GameWidget-Upcoming-Favorites-Percentage'); |
|
}); |
|
let existingBet = bettingElement[0].querySelector('.GameWidget-UpcomingBet'); |
|
// TODO more |
|
} |
|
} |
|
|
|
return teams.map( |
|
(x, i) => { |
|
let teamName = x.getText('.GameWidget-ScoreName'); |
|
return { |
|
teamId: x.href.substring('https://www.blaseball.com/team/'.length), |
|
teamName: teamName, |
|
record: x.getText('.GameWidget-ScoreRecord-WithBet') || x.getText('.GameWidget-ScoreRecord'), |
|
winChance: x.getText('.GameWidget-WinChance-WithBet') || x.getText('.GameWidget-WinChance') || upcomingOddsLookup[teamName], |
|
betAmount: x.getText('.GameWidget-ScoreBet-Amount'), |
|
possibleWinnings: x.getText('.GameWidget-ScoreBet-Winnings'), |
|
score: x.getNumber('.GameWidget-ScoreNumber'), |
|
}; |
|
}); |
|
} |
|
HTMLElement.prototype.getText = function(cssSelector) { |
|
const ele = this.querySelector(cssSelector); |
|
return ele && ele.textContent; |
|
}; |
|
HTMLElement.prototype.getNumber = function(cssSelector) { |
|
const ele = this.querySelector(cssSelector); |
|
if (ele && ele.textContent) { |
|
return parseFloat(ele.textContent); |
|
} |
|
return null; |
|
}; |
|
|
|
|
|
|
|
// ########## Log Coin Balance ########## // |
|
|
|
let reportedBalance = -1; |
|
function ReportCoinBalance() { |
|
const coinsElement = document.querySelector('.Navigation-CurrencyButton'); |
|
if (coinsElement) { |
|
const balance = parseInt(coinsElement.textContent); |
|
|
|
let eventType = 'coin-balance'; |
|
if (reportedBalance === -1) { |
|
eventType = 'coin-balance-startup'; |
|
} |
|
if (balance !== reportedBalance) { |
|
slackDataCollection({ type: eventType, value: balance }); |
|
slackLogBalance(balance); |
|
reportedBalance = balance; |
|
} |
|
} |
|
} |
|
|
|
|
|
// ########## Inject Game Result Data (Simulated and Real) ########## // |
|
|
|
let gameResultsLoggedData = undefined; |
|
// TODO the reading results part of this could be part of parsing the game cards generally |
|
function ReadResults() { |
|
let resultPanel = document.getElementById('ResultsSumId'); |
|
if (!resultPanel) { |
|
resultPanel = document.createElement('div'); |
|
resultPanel.className = 'ResultsSum'; |
|
resultPanel.id = 'ResultsSumId'; |
|
|
|
let insertUnder = document.querySelector('.LeagueNavigation-Nav'); |
|
if (!insertUnder) { |
|
console.log('ERROR: Cannot find insertion point for results panel'); |
|
return; |
|
} |
|
insertUnder.insertAdjacentElement('afterend', resultPanel); |
|
|
|
GM_addStyle(` |
|
.ResultsSum { |
|
text-align: center; |
|
font-weight: bold; |
|
padding-bottom: .8rem; |
|
white-space: pre; |
|
} |
|
`); |
|
} |
|
|
|
const [gainLoss, totalStake, totalWinnings, count, winCount] = ReadGainLoss(); |
|
|
|
// TODO parse the game cards, and only attempt to send this when all games are finalized?? |
|
let gameResults = { |
|
type: 'game-results-bets', |
|
count: count, |
|
gainLoss: gainLoss, |
|
totalStake: totalStake, |
|
totalWinnings: totalWinnings |
|
}; |
|
if (!isEquivalent(gameResults, gameResultsLoggedData)) { |
|
slackDataCollection(gameResults); |
|
gameResultsLoggedData = gameResults; |
|
} |
|
|
|
// Would have won:would have lost:total count (to know how much ambituity there might be) |
|
let winLossCount = `${winCount}::${count}`; |
|
|
|
resultPanel.textContent = `Latest Gain/Loss: ${gainLoss} (Staked: ${totalStake}, Winnings: ${totalWinnings}, Return: ${(gainLoss*100/totalStake).toFixed(2)}%) |
|
${winLossCount} |
|
${OtherAlgorithmResultsData()}`; |
|
} |
|
|
|
// ########## Collect Game Result Data (TODO-Make all of this use `parseGameResults`) ########## // |
|
|
|
function ReadGainLoss() { |
|
return ParseGainLoss([...document.querySelectorAll('.GameWidget-Outcome-Blurb')].map(x => x.innerText)); |
|
} |
|
function ParseGainLoss(messageArray) { |
|
var betLines = messageArray.filter(x => x.startsWith('You bet')); |
|
|
|
// alternate /^You bet (\d+) on the (.+?) and (won (\d+) coins.|lost.)$/ |
|
var parsed = betLines.map(x => /^You bet\W+(\d+)\Won the\W+(.+?)\Wand (won\W+(\d+)|lost)/.exec(x)).map(x => { |
|
let betAmount = parseInt(x[1]); |
|
let winnings = parseInt(x[4] || 0); |
|
return { |
|
BetAmount: betAmount, |
|
TeamName: x[2], |
|
Result: x[3], |
|
//Winnings: winnings ? winnings - betAmount : 0, |
|
Winnings: winnings, |
|
NetResult: (winnings) ? winnings-betAmount : -betAmount // Net is only the change, so if you broke even on a bet, it would be 0, not winnings |
|
}; |
|
}); |
|
|
|
var gainLoss = parsed.reduce((acc, cur) => acc + cur.NetResult, 0); |
|
var totalStake = parsed.reduce((acc, cur) => {acc += cur.BetAmount; return acc;}, 0); |
|
var totalWinnings = parsed.reduce((acc, cur) => acc + cur.Winnings, 0); |
|
|
|
var winCount = parsed.filter(x => x.Winnings > 0).length; |
|
|
|
return [gainLoss, totalStake, totalWinnings, parsed.length, winCount]; |
|
} |
|
function OtherAlgorithmResultsData() { |
|
let winLossData = [ |
|
ReadPercentageData(), |
|
...ReadGameResults() |
|
]; |
|
|
|
let mainOutput = winLossData.map(x => RenderWinLossData(x)); |
|
|
|
let fullOutput = [...mainOutput, ...winLossData.map(x => x.text)].join('\n'); |
|
return fullOutput; |
|
} |
|
function RenderWinLossData(data) { |
|
return `${data.wins}:${data.losses}:${data.games} - ${data.algorithm}`; |
|
} |
|
/// Return object: { wins: 3, losses: 2, games: 5, algorithm: '', text: '' } |
|
function ReadPercentageData() { |
|
// //let gameElements = document.querySelectorAll('.GameWidget-ScoreBacking'); |
|
// let gameElements = document.querySelectorAll('.GameWidget-Full-Live'); |
|
|
|
// gameElements.map(x => { |
|
// return { |
|
// TeamIds: x.querySelectorAll('.GameWidget-ScoreLine'), |
|
// Teams: x.querySelectorAll('.GameWidget-ScoreName'), |
|
// BetWinData: x.querySelector('.GameWidget-ScoreBet'), |
|
// PercentTeam1Win: x.querySelector('.GameWidget-WinChance-WithBet'), |
|
// }; |
|
// }).map(x => ({ |
|
// PercentTeam1Win: x.PercentTeam1Win && x.PercentTeam1Win.textContent, |
|
// BettingStake: x.BetWinData.innerText.split('\n')[0], |
|
// WinAmount: x.BetWinData.innerText.split('\n')[1] |
|
// })); |
|
let blurbs = document.querySelectorAll('.GameWidget-Outcome-Blurb'); |
|
let gameResults = blurbs.map(x => x.innerText).filter(x => x.endsWith('won the game.')); |
|
|
|
let regexMatches = gameResults.map(x => x.replace(/flavored/g, "favored")).map(x => /The ((heavily |heavy |mildly |)(favored\W|underdog\W))(.+?)\Wwon the game\./.exec(x)); |
|
|
|
// TODO mildly flavored is for Wings team... What do there? -- use the actual percentages?? |
|
// The heavily but mildly flavored Mild Wings won the game. |
|
//let failedToMatch = regexMatches. |
|
|
|
let parsed = regexMatches |
|
.filter(x => !!x) |
|
.map(x => ({ |
|
Rating: x[1].trim(), |
|
Favored: x[3].indexOf('favored') >= 0, |
|
Underdog: x[3].indexOf('underdog') >= 0, |
|
WinningTeam: x[4] |
|
})); |
|
|
|
// Would have won:would have lost:total count (to know how much ambituity there might be) |
|
//let winLoss = `${parsed.filter(x => x.Favored).length}:${parsed.filter(x => x.Underdog).length}:${parsed.length}`; |
|
let winLoss = { |
|
algorithm: 'Percents', |
|
wins: parsed.filter(x => x.Favored).length, |
|
losses: parsed.filter(x => x.Underdog).length, |
|
games: parsed.length, |
|
text: '' |
|
} |
|
|
|
let groupedPercentages = parsed.reduce((acc, cur) => { acc[cur.Rating] = acc[cur.Rating] ? acc[cur.Rating] + 1 : 1; return acc;}, {}); |
|
winLoss.text = Object.entries(groupedPercentages).join(" : "); |
|
|
|
return winLoss; |
|
} |
|
NodeList.prototype.map = Array.prototype.map; |
|
|
|
/// Return object: { wins: 3, losses: 2, games: 5, algorithm: '', text: '' } |
|
function ReadGameResults() { |
|
const gameElements = document.querySelectorAll('.GameWidget'); |
|
|
|
const gameResults = parseGameResults(gameElements); |
|
|
|
// TODO: gameResults.teams[].winPercent |
|
const denormalized = gameResults.map(x => { |
|
let team1 = x.teams[0]; |
|
let team2 = x.teams[1]; |
|
return { |
|
gameStatus: x.gameStatus, |
|
idOfWinner: team1.score > team2.score ? 1 : -1, |
|
idOfPercentLeader: compareWinChance(team1.winChance, team2.winChance), |
|
idOfRecordLeader: compareRecords(team1.record, team2.record), |
|
idOfStarLeader: compareStarIncludingShelled(team1.teamId, team2.teamId) |
|
}; |
|
}); |
|
|
|
const toConsider = denormalized.filter(x => x.gameStatus.indexOf('Final') >= 0); |
|
|
|
|
|
let validByStarsIncShelled = toConsider.filter(x => x.idOfStarLeader === 1 || x.idOfStarLeader === -1).length; |
|
let winsByStarsIncShelled = toConsider.filter(x => x.idOfStarLeader === x.idOfWinner).length; |
|
|
|
|
|
|
|
// let debugResults = gameResults.map(x => { |
|
// let team1 = x.teams[0]; |
|
// let team2 = x.teams[1]; |
|
// return `FFFFF: ${team1.winChance} vs ${team2.winChance} => ${compareWinChance(team1.winChance, team2.winChance)} == ${team1.score > team2.score ? 1 : -1} (scores: ${team1.score} > ${team2.score} ------- FFFFFFF: ${typeof(team1.score)} ${typeof(team2.score)} -> ${team1.score > team2.score}`; |
|
// }); |
|
// console.log(debugResults); |
|
|
|
|
|
let validByPercent = toConsider.filter(x => x.idOfPercentLeader === 1 || x.idOfPercentLeader === -1).length; |
|
let winsByPercent = toConsider.filter(x => x.idOfPercentLeader === x.idOfWinner).length; |
|
|
|
|
|
|
|
let validByRecord = toConsider.filter(x => x.idOfRecordLeader === 1 || x.idOfRecordLeader === -1).length; |
|
let winsByRecord = toConsider.filter(x => x.idOfRecordLeader === x.idOfWinner).length; |
|
|
|
// TODO this has a higher than expected number of "invalid" results... look into that |
|
|
|
|
|
return [ |
|
{ |
|
algorithm: 'StarRatingIncludingShelled', |
|
wins: winsByStarsIncShelled, |
|
losses: validByStarsIncShelled - winsByStarsIncShelled, |
|
games: validByStarsIncShelled |
|
}, |
|
{ |
|
algorithm: 'PercentValues', |
|
wins: winsByPercent, |
|
losses: validByPercent - winsByPercent, |
|
games: validByPercent |
|
}, |
|
{ |
|
algorithm: 'Record', |
|
wins: winsByRecord, |
|
losses: validByRecord - winsByRecord, |
|
games: validByRecord |
|
} |
|
]; |
|
} |
|
function compareWinChance(team1, team2) { |
|
const result = team1.localeCompare(team2); |
|
// if (result === 0) { |
|
// console.log(`DEBUG: compareWinChance - returned compare=${result} - ${team1} :: ${team2} -> `); |
|
// } |
|
return result; |
|
} |
|
function compareRecords(team1, team2) { |
|
const result = parseRecord(team1).toString().localeCompare(parseRecord(team2).toString()); |
|
// if (result === 0) { |
|
// console.log(`DEBUG: compareRecords - returned compare=${result} - ${team1} :: ${team2} -> ${parseRecord(team1)} :: ${parseRecord(team2)}`); |
|
// } |
|
return result; |
|
} |
|
function parseRecord(record) { |
|
let parsed = record.split('-').map(x => parseInt(x)); |
|
|
|
if (parsed[0] === 0) { |
|
return 0; |
|
} |
|
if (parsed[1] === 0) { |
|
return 1; |
|
} |
|
return parsed[0] / (parsed[0] + parsed[1]); |
|
} |
|
function compareStarIncludingShelled(teamId1, teamId2) { |
|
var team1Data = GetTeam(teamId1); |
|
var team2Data = GetTeam(teamId2); |
|
|
|
var team1Stars = team1Data.TeamStars + team1Data.ShelledStars; |
|
var team2Stars = team2Data.TeamStars + team2Data.ShelledStars; |
|
|
|
// If its within a certain range, don't use the stars |
|
var starDifference = Math.abs(team1Stars - team2Stars); |
|
|
|
if (starDifference >= 1) { |
|
return team1Stars > team2Stars |
|
? -1 |
|
: 1; |
|
} |
|
// Not within the difference amount, return "equal" |
|
return 0; |
|
} |
|
|
|
|
|
|
|
|
|
// ########## Helper Functions ########## // |
|
|
|
// https://stackoverflow.com/a/52486921/356218 |
|
function setNativeValue(element, value) { |
|
let lastValue = element.value; |
|
element.value = value; |
|
let event = new Event("input", { target: element, bubbles: true }); |
|
// React 15 |
|
event.simulated = true; |
|
// React 16 |
|
let tracker = element._valueTracker; |
|
if (tracker) { |
|
tracker.setValue(lastValue); |
|
} |
|
element.dispatchEvent(event); |
|
} |
|
function isEquivalent(a, b) { |
|
if (a === b) return true; |
|
// TODO this is probably sketchy logic |
|
if (!a || !b) return false; |
|
|
|
// Create arrays of property names |
|
var aProps = Object.getOwnPropertyNames(a); |
|
var bProps = Object.getOwnPropertyNames(b); |
|
|
|
// If number of properties is different, |
|
// objects are not equivalent |
|
if (aProps.length != bProps.length) { |
|
return false; |
|
} |
|
|
|
for (var i = 0; i < aProps.length; i++) { |
|
var propName = aProps[i]; |
|
|
|
// If values of same property are not equal, |
|
// objects are not equivalent |
|
if (a[propName] !== b[propName]) { |
|
return false; |
|
} |
|
} |
|
|
|
// If we made it this far, objects |
|
// are considered equivalent |
|
return true; |
|
} |
|
|
|
|
|
|
|
|
|
// ########## Log Countdown Events ########## // |
|
|
|
function catchCountdownFinish() { |
|
const countdownValue = getCountdownText(); |
|
const countdownIsWithinRange = (countdownValue && countdownValue.indexOf(`Hours0Minutes`) >= 0); |
|
|
|
if (countdownIsWithinRange && !countDownLoopId) { |
|
console.log('INFO: Starting watching for countdown to reach zero'); |
|
countDownLoopId = setInterval(countdownFinishLoop, 300); |
|
} |
|
} |
|
let countDownLoopId = null; |
|
let countDownSentTime = null; |
|
function countdownFinishLoop() { |
|
const countdownValue = getCountdownText(); |
|
const shouldSendNow = (countdownValue && countdownValue.indexOf(`Hours0Minutes0Seconds`) >= 0); |
|
|
|
if (shouldSendNow) { |
|
if (!countDownSentTime || (new Date() - countDownSentTime) > 30000) { |
|
// Check says we need to send, and confirmed it hasn't been sent shortly ago |
|
// So actually send: |
|
slackDataCollection({ type: 'countdown-done', value: countdownValue }); |
|
slackLogCollection('INFO', `Countdown hit zero: ${countdownValue}`); |
|
} |
|
// Make sure we reset as the default state |
|
countDownSentTime = new Date(); |
|
countDownLoopId = clearInterval(countDownLoopId); |
|
} |
|
else if (!countdownValue) { |
|
// Did not find a countdown... what do? stop loop? |
|
countDownLoopId = clearInterval(countDownLoopId); |
|
} |
|
} |
|
function getCountdownText() { |
|
const countdownElement = document.querySelector('.Countdown'); |
|
return countdownElement && countdownElement.textContent; |
|
} |
|
|
|
|
|
|
|
|
|
// ########## Log Data Helper Functions ########## // |
|
|
|
// TODO: https://esdiscuss.org/topic/maps-with-object-keys ?? |
|
let dataCollectionUniqueLookup = {}; |
|
function dataCollectionUnique(eventType, dataArray, clearOnEmpty = true) { |
|
if (!dataCollectionUniqueLookup[eventType]) { |
|
dataCollectionUniqueLookup[eventType] = new Map(); |
|
} |
|
let mapRef = dataCollectionUniqueLookup[eventType]; |
|
|
|
if (clearOnEmpty && dataArray.length === 0) { |
|
// Passed array is empty, clear the seen history |
|
dataCollectionUniqueLookup[eventType] = new Map(); |
|
return; |
|
} |
|
|
|
let thisLoopSeen = new Map(); |
|
dataArray.forEach((tIn) => { |
|
const t = JSON.stringify(tIn); |
|
let thisCount = (thisLoopSeen.get(t) || 0) + 1; |
|
thisLoopSeen.set(t, thisCount); |
|
|
|
let alreadyReported = (mapRef.get(t) || 0); |
|
//console.log(`DEBUG: dataCollectionUnique: ${eventType} count=${thisCount} > reported=${alreadyReported} :: ${typeof(t)} == ${t}`); |
|
while (thisCount > alreadyReported) { |
|
++alreadyReported; |
|
// Send the message now |
|
//console.log(`INFO: sending event type to slack: ${eventType}`); |
|
slackDataCollection({ type: eventType, value: t }); |
|
mapRef.set(t, alreadyReported); |
|
} |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ########## Log Data - Implementation ########## // |
|
|
|
let slackConfigMessage = true; |
|
function slackDataCollection(obj) { |
|
const dataUrlAndToken = localStorage.getItem("!slackWebhookUrlAndToken"); |
|
const codeBlock = '```'; |
|
|
|
if (dataUrlAndToken) { |
|
//`curl -X POST -H 'Content-type: application/json' --data '{"text":"Hello, World!"}' ${urlAndToken}` |
|
postData(dataUrlAndToken, { text: `${codeBlock}${JSON.stringify(obj, null, 2)}${codeBlock}`}); |
|
} |
|
else if (slackConfigMessage) { |
|
console.log("ERROR: Slack webhook url isn't configured, will not collect data"); |
|
console.log(" To configure, in console call: localStorage.setItem('!slackWebhookUrlAndToken', '<your webhook url and token>');"); |
|
slackConfigMessage = false; |
|
} |
|
} |
|
function slackLogCollection(level, msg) { |
|
const logsUrlAndToken = localStorage.getItem("!slackWebhookLogs"); |
|
if (logsUrlAndToken) { |
|
switch (level.toLocaleLowerCase()) { |
|
case 'error': |
|
level = `:fire: ${level}`; |
|
break; |
|
case 'debug': |
|
level = `:hear_no_evil: ${level}`; |
|
break; |
|
} |
|
|
|
postData(logsUrlAndToken, { text: `${level}: ${msg}`}); |
|
} |
|
else if (slackConfigMessage) { |
|
console.log("ERROR: Slack logging webhook url isn't configured, will not collect logs"); |
|
console.log(" To configure, in console call: localStorage.setItem('!slackWebhookLogs', '<your webhook url and token>');"); |
|
slackConfigMessage = false; |
|
} |
|
} |
|
function slackLogBalance(bal) { |
|
const logsUrlAndToken = localStorage.getItem("!slackWebhookLogBalance"); |
|
if (logsUrlAndToken) { |
|
postData(logsUrlAndToken, { text: `${bal}` }); |
|
} |
|
else if (slackConfigMessage) { |
|
console.log("ERROR: Slack logging webhook url isn't configured, will not collect logs"); |
|
console.log(" To configure, in console call: localStorage.setItem('!slackWebhookLogBalance', '<your webhook url and token>');"); |
|
slackConfigMessage = false; |
|
} |
|
} |
|
function postData(url, data) { |
|
// construct an HTTP request |
|
var xhr = new XMLHttpRequest(); |
|
xhr.addEventListener('loadend', handleEvent); |
|
xhr.addEventListener('error', handleEvent); |
|
|
|
xhr.open('POST', url, true); |
|
// Apparently slackd oesn't support CORS, and not having this header doesn't check in the same way |
|
//xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); |
|
|
|
// send the collected data as JSON |
|
xhr.send(JSON.stringify(data)); |
|
|
|
return xhr; |
|
}; |
|
function handleEvent(e) { |
|
if (e.type === 'error') { |
|
console.log('ERROR: postData failed', e); |
|
return; |
|
} |
|
if (e.type === 'loadend') { |
|
console.log('DEBUG: postData success', e); |
|
return; |
|
} |
|
} |