// ==UserScript== // @name onlinetichu.com card counter // @version 1.4.0 // @description automatically count cards at onlinetichu.com // @match *://*.onlinetichu.com/Site/Game/Table/* // @author Maximilian Keßler // @namespace https://gitlab.com/kesslermaximilian/onlinetichu-counter // @license MIT // ==/UserScript== // disable auto fold functionality completely function injectCounter() { if (ot.AutoFold) { ot.$autoFold.click() ot.$autoFold.hide() } var myStyles = ` #playedHigh { position: absolute; top: 45px; left: 5px; width: 935px; height: 120px; } #playedLow { position: absolute; top: 170px; left: 5px; width: 935px; height: 120px; } #counterField { position: absolute; width: 945px; height: 300px; top: 635px; border: 1px solid black; box-shadow: 1px 1px 1px #555; background: #88CC00; } #controlBar { position: absolute; min-height: 32px; width: 925px; top: 5px; left: 5px; border-radius: 20px; background: #000000; } #controlButtonsLeft { position: absolute; top: 4px; left: 15px; } #controlButtonsRight { position: absolute; top: 4px; right: 15px; } .playedCardsLayout { position: absolute; height: 125px; } .playedCardsLayout .card { width: 80px; height: 120px; border-radius: 5px; float: left; margin-left: -50px; text-align: left; box-shadow: 2px 2px 5px #555; border: 1px solid #000000; } .playedCardsLayout .card:hover { z-index: 1400; } ` var styleSheet = document.createElement("style") styleSheet.innerText = myStyles; console.log(styleSheet); document.head.appendChild(styleSheet); var controlBar = document.createElement('div'); controlBar.id = 'controlBar'; var controlButtonsLeft = document.createElement('div'); controlButtonsLeft.className = 'btn-group'; controlButtonsLeft.id = 'controlButtonsLeft'; var controlButtonsRight = document.createElement('div'); controlButtonsRight.className = 'btn-group'; controlButtonsRight.id = 'controlButtonsRight'; var davidProtectionButton = document.createElement('button'); davidProtectionButton.className = "btn btn-success btn-xs"; davidProtectionButton.id = "davidProtection"; davidProtectionButton.type = "button"; davidProtectionButton.innerHTML = 'David protection: Off'; var autoWishButton = document.createElement('button'); autoWishButton.className = "btn btn-info btn-xs"; autoWishButton.id = "autoWish"; autoWishButton.type = "button"; autoWishButton.innerHTML = 'Auto Wish: Off'; var showRemainingButton = document.createElement('button'); showRemainingButton.className = "btn btn-info btn-xs"; showRemainingButton.id = "showRemaining"; showRemainingButton.type = "button"; showRemainingButton.innerHTML = 'Show: Remaining'; var showOwnCardsButton = document.createElement('button'); showOwnCardsButton.className = "btn btn-primary btn-xs"; showOwnCardsButton.id = "showOwnCards"; showOwnCardsButton.type = "button"; showOwnCardsButton.innerHTML = 'OwnCards: Shown'; var showCountedCardsButton = document.createElement('button'); showCountedCardsButton.className = "btn btn-success btn-xs"; showCountedCardsButton.id = "showCountedCards"; showCountedCardsButton.type = "button"; showCountedCardsButton.innerHTML = 'Show cards: No'; controlButtonsRight.appendChild(showOwnCardsButton); controlButtonsRight.appendChild(showRemainingButton); controlButtonsRight.appendChild(showCountedCardsButton); controlButtonsLeft.appendChild(davidProtectionButton); controlButtonsLeft.appendChild(autoWishButton); controlBar.appendChild(controlButtonsLeft); controlBar.appendChild(controlButtonsRight); var counterField = document.createElement("div"); counterField.id = 'counterField'; counterField.appendChild(controlBar); // add an extra field where we will put down the played cards var cardsField = document.createElement('div'); cardsField.id = 'cardsField'; cardsField.style.marginLeft = '25px'; var playedHigh = document.createElement('div'); playedHigh.id = 'playedHigh'; playedHigh.style.marginLeft = '25px'; playedHigh.className = 'row'; playedHigh.hidden = true; var playedLow = document.createElement('div'); playedLow.id = 'playedLow'; playedLow.style.marginLeft = '25px'; playedLow.className = 'row'; playedLow.hidden = true; counterField.appendChild(playedHigh); counterField.appendChild(playedLow); tableIG = document.getElementById('gameField').parentNode; tableIG.appendChild(counterField); var createCardPlace = function(value, shift, node) { var playedCards = document.createElement('ul'); playedCards.id = 'playedCards' + value; // playedCards.className = 'layoutPlayedCards list-unstyled col-lg-2'; // playedCards.className = 'playedCardsLayout list-unstyled'; playedCards.className = 'playedCardsLayout list-unstyled'; playedCards.style.width = '160px'; playedCards.style.marginLeft = '25px'; playedCards.style.left = shift*127 + 'px'; node.appendChild(playedCards); } createCardPlace(1, 0, document.getElementById('playedHigh')); for(let i = 14; i > 8; i--) { createCardPlace(i, 14 - i + 1, document.getElementById('playedHigh')); } for(let i = 8; i > 1; i--) { createCardPlace(i, 8 - i, document.getElementById('playedLow')); } var smartFoldButton = document.createElement('button'); smartFoldButton.className = "btn btn-danger btn-xs"; smartFoldButton.id = "smartFold"; smartFoldButton.type = "button"; smartFoldButton.innerHTML = 'Smart fold: Off'; var showCardCounterButton = document.createElement('button'); showCardCounterButton.className = "btn btn-primary btn-xs"; showCardCounterButton.id = "showCardCounter"; showCardCounterButton.type = "button"; showCardCounterButton.innerHTML = 'Card Counter: No'; buttons = document.getElementById('statusButtons'); buttons.insertBefore(showCardCounterButton, document.getElementById('autoFold')); buttons.insertBefore(smartFoldButton, document.getElementById('autoFold')); var patchPlayerStats = function(direction) { playerLevel = document.getElementById(direction + 'PlayerLevel'); playerGoldTourns = document.getElementById(direction + 'PlayerGoldTourns'); playerTichu = document.getElementById(direction + 'PlayerTichu'); playerDiv = playerLevel.parentElement; playerDiv.removeChild(playerLevel); playerDiv.removeChild(playerGoldTourns); playerMyLevel = document.createElement('div'); playerMyLevel.id = direction + 'PlayerMyLevel'; playerMyLevel.innerText = ''; playerTichuCoefficient = document.createElement('div'); playerTichuCoefficient.id = direction + 'PlayerTichuCoefficient'; playerTichuCoefficient.innerText = ''; playerDiv.insertBefore(playerMyLevel, playerTichu); playerDiv.insertBefore(playerTichuCoefficient, playerTichu); } patchPlayerStats('south'); patchPlayerStats('north'); patchPlayerStats('east'); patchPlayerStats('west'); // playedCards. // ot.$gameField.insertAfter(playedCards, ot.$gameField.lastChild); // set up counting of played cards var OnlineTichuCounter = { $playedCards: {}, $smartFold: $('#smartFold'), $smartFoldValue: $('#smartFoldValue'), $showCardCounter: $('#showCardCounter'), $showCardCounterValue: $('#showCardCounterValue'), $showRemaining: $('#showRemaining'), $showRemainingValue: $('#showRemainingValue'), $showOwnCards: $('#showOwnCards'), $showOwnCardsValue: $('#showOwnCardsValue'), $showCountedCards: $('#showCountedCards'), $showCountedCardsValue: $('#showCountedCardsValue'), $davidProtection: $('#davidProtection'), $autoWishValue: $('#autoWishValue'), $autoWish: $('#autoWish'), $davidProtectionValue: $('#davidProtectionValue'), $playedHigh: $('#playedHigh'), $playedLow: $('#playedLow'), $counterField: $('#counterField'), $playerMyLevel: { 'east': $('#eastPlayerMyLevel'), 'north': $('#northPlayerMyLevel'), 'west': $('#westPlayerMyLevel'), 'south': $('#southPlayerMyLevel'), }, $playerTichuCoefficient: { 'east': $('#eastPlayerTichuCoefficient'), 'north': $('#northPlayerTichuCoefficient'), 'west': $('#westPlayerTichuCoefficient'), 'south': $('#southPlayerTichuCoefficient'), }, // dynamic game state allCards: {}, playedCards: {}, totalPlayed: [], exchangedTo: {}, exchangedFrom: {}, handCardShuffle: [], playerNames: {}, ownCards: [], // config values, triggered with buttons smartFold: false, showCardCounter: false, showOwnCards: true, showRemaining: true, showCountedCards: false, davidProtection: false, autoWish: false, shuffleCards: true, // dynamic feature state davidProtectionTriggered: true, bombAfterFold: null, // to implement stats: {}, util: { displayCardsHtmlString: function(cards) { htmlString = ""; $(cards).each(function () { var value = this.Value; if (this.Shape == 7) { value = 0; } htmlString += '
  • '; }); return htmlString; }, populateAllCards: function() { for(let val = 2; val < 15; val++) { otc.allCards[val] = []; for(let shape = 0; shape < 4; shape++) { otc.allCards[val].push({ Shape: shape, Value: val }); } } otc.allCards[1] = [ { Shape: 4, Value: 1 }, // mahjong { Shape: 5, Value: 0 }, // dog { Shape: 6, Value: 15}, // dragon { Shape: 7, Value: 14 } // phoenix ]; }, cardInArray: function(card, array) { contained = false; for(let i = 0; i < array.length; i++) { if (array[i].Shape == card.Shape && array[i].Value == card.Value) { contained = true; } } return contained; }, extractInt: function(text) { num = /\d+(m|k)?/.exec(text)[0]; if (num.includes('k')) { return 1000 * parseInt(num.split('k')[0]); } else if (num.includes('m')) { return 1000000 * parseInt(num.split('m')[0]); } return parseInt(num); }, extractFloat: function(text) { num = /\d+(\.\d+)?/.exec(text)[0]; return parseFloat(num); }, // warning: note that this is async getStats: function(user) { $.get('https://www.onlinetichu.com/Site/Profiles/User/' + user, null, function(text){ statsNav = $(text).find('#nav-statistics'); wbox1 = statsNav.children()[1]; c1 = wbox1.children[0].children[0].children[1].children; c2 = wbox1.children[0].children[0].children[2].children; var generalStats = { level: otc.util.extractInt( c1[0].innerText), nextLevelIn: otc.util.extractFloat( c1[1].innerText), rating: otc.util.extractInt( c1[2].innerText), games: otc.util.extractFloat( c1[3].innerText), wins: otc.util.extractFloat( c1[4].innerText.split("-")[0]), defeats: otc.util.extractFloat( c1[4].innerText.split("-")[1]), winningPercentage: (otc.util.extractFloat( c1[5].innerText)).toFixed(1), goldGames: otc.util.extractFloat( c1[6].innerText), goldWins: otc.util.extractFloat( c1[7].innerText.split("-")[0]), goldDefeats: otc.util.extractFloat( c1[7].innerText.split("-")[1]), goldWinningPercentage: otc.util.extractFloat( c1[8].innerText), points: otc.util.extractInt( c1[9].innerText), rounds: otc.util.extractInt( c1[10].innerText), oneTwo: otc.util.extractInt( c1[11].innerText), grandTichuPercentage: otc.util.extractFloat( c2[0].innerText), grandTichuCalled: otc.util.extractInt( c2[1].innerText), grandTichuSuccessful: otc.util.extractInt( c2[2].innerText), tichuPercentage: otc.util.extractFloat( c2[3].innerText), tichuCalled: otc.util.extractInt( c2[4].innerText), tichuSuccessful: otc.util.extractInt( c2[5].innerText), tournaments: otc.util.extractInt( c2[6].innerText), tournamentsFirstAward: otc.util.extractInt( c2[7].innerText), tournamentsSecondAward: otc.util.extractInt( c2[8].innerText), abandonments: otc.util.extractInt( c2[9].innerText) }; lostGrand = generalStats.grandTichuCalled - generalStats.grandTichuSuccessful; lostTichu = generalStats.tichuCalled - generalStats.tichuSuccessful; var customStats = { abandonmentRate: (generalStats.abandonments / (generalStats.games + generalStats.goldGames) * 100).toFixed(1), grandTichuUnsuccesful: lostGrand, tichuUnsuccesful: lostTichu, tichuCoefficient: (( (generalStats.grandTichuSuccessful - lostGrand) * 200 + (generalStats.tichuSuccessful - lostTichu) * 100 ) / generalStats.rounds).toFixed(1), oneTwoCoefficient: (100 * generalStats.oneTwo / generalStats.rounds).toFixed(1) // note that we only use 100 here, as a One-Two is scored for both players. This way, this is better comparable to the tichuCoefficient } var userStats = { custom: customStats, general: generalStats } otc.stats[user] = userStats ; // console.log('Fetched stats of user ' + user); // update stats shown at table otc.action.updateDisplayStats(); }); } }, action: { selectAutoWish: function() { if (otc.exchangedTo['east'] == null) { return; } val = 0; if (otc.exchangedTo['east'].Shape < 4 ) { val = otc.exchangedTo['east'].Value; } else if (otc.exchangedTo['east'].Shape == 5) { val = 14; // wish for ace by default if we gave a dog to the right } button = document.getElementById("askCard_" + val); button.click(); }, deSelectAutoWish: function() { button = document.getElementById("askCard_0"); button.click(); }, handlePlayedCard: function(card) { index = 0; // special cards have Shapes 4,5,6,7. We group them at value 1 if(card.Shape >= 4) { index = 1; if (card.Shape == 7) { // Treat phoenix as if it had value 14 // This is important for comparisons later card.Value = 14; } } else { index = card.Value; } otc.playedCards[index].push(card); otc.totalPlayed.push(card); otc.playedCards[index].sort(function(c1, c2){ return c2.Shape - c1.Shape; }); }, handlePlayedCards: function(cards) { if (cards.length === 0) { return; } if (!otc.util.cardInArray(cards[0], otc.totalPlayed)) { $(message.Table.TableCards).each( (index, card) => otc.action.handlePlayedCard(card) ); } }, reset: function() { for(let i = 1; i < 15; i++) { otc.playedCards[i] = []; } dirs = ['east', 'north', 'west']; for (i in dirs) { otc.exchangedTo[dirs[i]] = null; otc.exchangedFrom[dirs[i]] = null; } otc.totalPlayed = []; otc.handCardShuffle = []; otc.playerNames = {}; otc.stats = {}; }, drawPlayedCards: function() { for(let i = 1; i < 15; i++) { if (otc.showRemaining) { cards = []; for(let j=0; j < otc.allCards[i].length; j ++) { if(!otc.util.cardInArray(otc.allCards[i][j], otc.playedCards[i]) && (otc.showOwnCards || !otc.util.cardInArray(otc.allCards[i][j], otc.ownCards))) { cards.push(otc.allCards[i][j]); } otc.$playedCards[i].html(otc.util.displayCardsHtmlString(cards)); } } else { otc.$playedCards[i].html(otc.util.displayCardsHtmlString(otc.playedCards[i])); } } }, updateDisplayStats: function() { dirs = ['east', 'north', 'south', 'west']; for(i in dirs) { stats = otc.stats[otc.playerNames[dirs[i]]]; if (stats != undefined) { t = 'L' + stats.general.level + ' A' + stats.custom.abandonmentRate + ' W' + stats.general.winningPercentage + ''; otc.$playerMyLevel[dirs[i]].text(t); t = 'T' + stats.custom.tichuCoefficient + ' O' + stats.custom.oneTwoCoefficient; otc.$playerTichuCoefficient[dirs[i]].text(t); } else { otc.$playerMyLevel[dirs[i]].text(''); otc.$playerTichuCoefficient[dirs[i]].text(''); } } // console.log('updated shown stats'); } } } if (!window.otc) { window.otc = OnlineTichuCounter; } otc.$counterField.hide(); // add functionality to smartFold button otc.$smartFold.click(function () { otc.smartFold = !otc.smartFold; if (otc.smartFold) { otc.$smartFoldValue.text(otc.$smartFoldValue.data('on')); } else { otc.$smartFoldValue.text(otc.$smartFoldValue.data('off')); } }); // add functionality to show button otc.$showCardCounter.click(function () { otc.showCardCounter = !otc.showCardCounter; if (otc.showCardCounter) { otc.$showCardCounterValue.text(otc.$showCardCounterValue.data('on')); otc.$counterField.show(); } else { otc.$showCardCounterValue.text(otc.$showCardCounterValue.data('off')); otc.$counterField.hide(); } }); otc.$showCountedCards.click(function () { otc.showCountedCards = !otc.showCountedCards; if (otc.showCountedCards) { otc.$showCountedCardsValue.text(otc.$showCountedCardsValue.data('on')); otc.$playedHigh.show(); otc.$playedLow.show(); } else { otc.$showCountedCardsValue.text(otc.$showCountedCardsValue.data('off')); otc.$playedHigh.hide(); otc.$playedLow.hide(); } }); otc.$davidProtection.click(function () { otc.davidProtection = !otc.davidProtection; if (otc.davidProtection) { otc.$davidProtectionValue.text(otc.$davidProtectionValue.data('on')); } else { otc.$davidProtectionValue.text(otc.$davidProtectionValue.data('off')); } }); otc.$autoWish.click(function () { otc.autoWish = !otc.autoWish; if (otc.autoWish) { otc.$autoWishValue.text(otc.$autoWishValue.data('on')); otc.action.selectAutoWish(); } else { otc.$autoWishValue.text(otc.$autoWishValue.data('off')); otc.action.deSelectAutoWish(); } }); otc.$showRemaining.click(function () { otc.showRemaining = !otc.showRemaining; if (otc.showRemaining) { otc.$showRemainingValue.text(otc.$showRemainingValue.data('on')); } else { otc.$showRemainingValue.text(otc.$showRemainingValue.data('off')); } otc.action.drawPlayedCards(); }); otc.$showOwnCards.click(function () { otc.showOwnCards = !otc.showOwnCards; if (otc.showOwnCards) { otc.$showOwnCardsValue.text(otc.$showOwnCardsValue.data('on')); } else { otc.$showOwnCardsValue.text(otc.$showOwnCardsValue.data('off')); } otc.action.drawPlayedCards(); }); for(let i = 1; i < 15; i++) { otc.$playedCards[i] = $('#playedCards' + i); } otc.action.reset(); otc.util.populateAllCards(); // overwrite functionality of fold button ot.$fold.unbind(); ot.$fold.hide().click(function () { if (otc.davidProtection) { if(ot.$passCards().length != 0 && !otc.davidProtectionTriggered) { console.log('david protection triggered'); ot.$gameMessage.text("Selected cards detected. Press again to fold!"); ot.HorizontalAlign(ot.$gameMessage); otc.davidProtectionTriggered = true; return; } } ot.action.Fold(); otc.davidProtectionTriggered = false; $(this).hide(); }); MyTableState = (function() { var cachedFunction = ot.reaction.TableState; return function() { var result = cachedFunction.apply(this, arguments); message = arguments[0]; // console.log(message); // console.log('detected table state change event'); // console.log(message.Table.Players); // reset played cards at start of round if(message.Table.Status == 'WaitForNextCards' && otc.totalPlayed.length != 0) { otc.action.reset(); } if(message.Table.Status == 'Playing') { otc.action.handlePlayedCards(message.Table.TableCards); } otc.action.drawPlayedCards(); // console.log('reached smartFold point'); spectator = true; // reset player names as they might have changed otc.playerNames = {}; southPosition = 1; $(message.Table.Players).each(function () { if (this.Username == ot.$Username.val()) { otc.ownCards = this.Cards; spectator = false; southPosition = this.Position; // console.log('found player'); if (message.Table.Turn == this.Position) { // console.log('my turn!'); if (this.Cards.length < 4 && this.Cards.length < message.Table.TableCards.length) { if (otc.smartFold) { if(this.MustFold) { ot.action.Fold(); console.log('smart fold activated'); } else { console.log('prevented bug by not smartfolding'); } } } } } // get stats of player if we don't have them yet) if (otc.stats[this.Username] === undefined) { otc.util.getStats(this.Username); } }); $(message.Table.Players).each(function () { dirs = ['south', 'east', 'north', 'west']; index = (this.Position - southPosition + 4) % 4; otc.playerNames[dirs[index]] = this.Username; }); otc.action.updateDisplayStats(); return result; } })(); MyChat = (function() { var cachedFunction = ot.reaction.Chat; return function() { var result = cachedFunction.apply(this, arguments); message = arguments[0]; if (message.User.Username === "System") { var messageArray = message.Text.replace(//g, '>').split(' '); message.Text = messageArray.join(' '); messageArray = message.Text.replace(//g, '>').split(' '); for(var i in messageArray) { if(messageArray[i] == "dogs") { otc.action.handlePlayedCard({ "Shape": 5, "Value": 0 }); } } } return result; } })(); MyExchangeCards = (function() { var cachedFunction = ot.action.ExchangeCards; return function() { otc.exchangedTo['west'] = { Shape: ot.$westExchange.children('li:nth-child(1)').data('shape'), Value: ot.$westExchange.children('li:nth-child(1)').data('value') }; otc.exchangedTo['north'] = { Shape: ot.$northExchange.children('li:nth-child(1)').data('shape'), Value: ot.$northExchange.children('li:nth-child(1)').data('value') }; otc.exchangedTo['east'] = { Shape: ot.$eastExchange.children('li:nth-child(1)').data('shape'), Value: ot.$eastExchange.children('li:nth-child(1)').data('value') }; console.log(otc.exchangedTo); if(otc.autoWish) { otc.action.selectAutoWish(); } else { otc.action.deSelectAutoWish(); } var result = cachedFunction.apply(this, arguments); return result; } })(); MyReviewExchange = (function() { var cachedFunction = ot.reaction.ReviewExchange; return function() { var result = cachedFunction.apply(this, arguments); return result; } })(); ot.reaction.TableState = MyTableState; ot.reaction.Chat = MyChat; ot.action.ExchangeCards = MyExchangeCards; }; // Special care is to be taken to safely inject our javascript into the website // without the website being able to talk back to greasemonkey // (which would be a security problem) // see // https://stackoverflow.com/questions/5006460/userscripts-greasemonkey-calling-a-websites-javascript-functions // and the excellent answer by Wayne for details on this function exec(fn) { var script = document.createElement('script'); script.setAttribute("type", "application/javascript"); script.textContent = '(' + fn + ')();'; document.body.appendChild(script); // run the script document.body.removeChild(script); // clean up } exec(injectCounter);