Skip to content

Instantly share code, notes, and snippets.

@Eibwen
Last active March 9, 2021 22:06
Show Gist options
  • Save Eibwen/012ec42416dd4749d5f4e0db6b162ef5 to your computer and use it in GitHub Desktop.
Save Eibwen/012ec42416dd4749d5f4e0db6b162ef5 to your computer and use it in GitHub Desktop.
Blaseball Helper

Legend:

  • [+] Means a new feature or other addition of logic
  • [%] Means refactoring
  • [-] Means removed code or other cleanup
  • [&] TODO item or thought
  • Bug fix

1.4.2

Largely bug fixes

  • [+] Using JSON.stringify as the keys for the Map to know if I've reported an data object to slack yet (kinda lazy, but hey)
  • [+] Got trackGameResults fully working, due to the stringify as the key
  • Fixed a bug that I'm surprised wasn't noticed yet, issue with bettingElement being an array not a HTMLElement
  • Fixed bug on comparing scores, where it would compare as string. Therefore the logic was saying "11" is less than "2"
  • [&] Worried other bugs like this might exist, so exploring what it takes to convert to typeScript
  • Bug due to "home team" apparently having the game status include the innings, so would say Final (11), where everything else was always just Final
  • [-] Disabled some debugging lines

1.4.1

  • [+] Added section labels (in the format of // ########## {label} ########## //) in the chance that anyone else would be trying to figure out this script
  • [+] Started keeping this changelog, lets see if it is more updated than "Version Notes"

1.4

  • [+] Stopped calculating "Shelled" players as part of the TeamStars (currently just holding that count separately, without data to reconstruct Mean Median or other player-based metrics)
  • [+] Added simulated output of the results when including "Shelled" players
  • [%] Removed PlaceBetsScreen function as it served no purpose anymore
  • [+] Moving StarRating calculation and injection to work on any page with GameWidget-ScoreLine elements
  • [+] Found a seemingly reasonable way to make the Mildly Flavored Mild Wings results be parsed more consistently

1.3

Mostly exploring new things, and trying to make newer features more reliable

  • [+] Getting betting information from GameWidget cards
  • [+] Making Game Result data get parsed from GameWidget cards from the Watch Live screen too
  • [+] Added simulation of betting strategies to the GameResults logic
  • [+] Added "Version Notes", which this changelog is replacing (as of 5 days later and 1.4.1)
  • [&] More data can be parsed from GameWidget cards depending on the screens
  • [&] Switch over to new betting state machine (and debug any of that)
  • [&] implemnet a GetHashCode in the Unique events
  • [&] Hook up better fallbacks to make sure betting always happens
  • [&] should know how to request the "Begging" option in the shop when the balance hits 0

Stopped back-filling this log at this point

// ==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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment