// ==UserScript==
// @name Torn Blackjack Assist with Bot (Module)
// @namespace torn.blackjack.assist.bot
// @version 4.0
// @description Displays real-time basic strategy advice for Blackjack on Torn with optional auto-play bot
// @match https://www.torn.com/page.php?sid=blackjack*
// @match https://www.torn.com/pda.php*step=blackjack*
// @match https://www.torn.com/*
// @grant unsafeWindow
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const globalWindow = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
const MODULE_NAME = "BlackjackAssist";
let active = false;
let observer = null;
let botEnabled = false;
let autoPlayDelay = 2000; // 2 second delay between actions
let betAmount = 10;
let maxLossStreak = 5;
let currentLossStreak = 0;
let stopOnTokens = 5; // Stop when tokens reach this number
let botStats = { wins: 0, losses: 0, totalHands: 0, startingTokens: 0, currentTokens: 0 };
let lastConfirmationTime = 0; // Prevent rapid clicking on confirmations
let ignoreConfirmationsUntil = 0; // Ignore confirmations for a period after clicking
let lastActionTime = 0; // Prevent rapid actions
let waitingForAction = false; // Flag to prevent multiple simultaneous actions
// ================= Logging =================
function log(msg, type = "info") {
if (globalWindow.TornFramework?.log) {
globalWindow.TornFramework.log(msg, type, MODULE_NAME);
} else {
console.log(`[${MODULE_NAME}] ${type.toUpperCase()}: ${msg}`);
}
}
// ================= Strategy Tables =================
const A = { HIT: 'Hit', STAND: 'Stand', DOUBLE: 'Double', SPLIT: 'Split' };
const strategy = {
hard: {
8: {1:A.HIT,2:A.HIT,3:A.HIT,4:A.HIT,5:A.HIT,6:A.HIT,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
9: {1:A.HIT,2:A.DOUBLE,3:A.DOUBLE,4:A.DOUBLE,5:A.DOUBLE,6:A.DOUBLE,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
10: {1:A.DOUBLE,2:A.DOUBLE,3:A.DOUBLE,4:A.DOUBLE,5:A.DOUBLE,6:A.DOUBLE,7:A.DOUBLE,8:A.DOUBLE,9:A.DOUBLE,10:A.HIT,11:A.HIT},
11: {1:A.DOUBLE,2:A.DOUBLE,3:A.DOUBLE,4:A.DOUBLE,5:A.DOUBLE,6:A.DOUBLE,7:A.DOUBLE,8:A.DOUBLE,9:A.DOUBLE,10:A.DOUBLE,11:A.DOUBLE},
12: {1:A.HIT,2:A.HIT,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
13: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
14: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
15: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
16: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
17: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.STAND,8:A.STAND,9:A.STAND,10:A.STAND,11:A.STAND},
18: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.STAND,8:A.STAND,9:A.STAND,10:A.STAND,11:A.STAND},
19: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.STAND,8:A.STAND,9:A.STAND,10:A.STAND,11:A.STAND},
20: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.STAND,8:A.STAND,9:A.STAND,10:A.STAND,11:A.STAND},
21: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.STAND,8:A.STAND,9:A.STAND,10:A.STAND,11:A.STAND}
},
soft: {
13: {1:A.HIT,2:A.HIT,3:A.HIT,4:A.DOUBLE,5:A.DOUBLE,6:A.DOUBLE,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
14: {1:A.HIT,2:A.HIT,3:A.HIT,4:A.DOUBLE,5:A.DOUBLE,6:A.DOUBLE,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
15: {1:A.HIT,2:A.HIT,3:A.DOUBLE,4:A.DOUBLE,5:A.DOUBLE,6:A.DOUBLE,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
16: {1:A.HIT,2:A.HIT,3:A.DOUBLE,4:A.DOUBLE,5:A.DOUBLE,6:A.DOUBLE,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
17: {1:A.HIT,2:A.DOUBLE,3:A.DOUBLE,4:A.DOUBLE,5:A.DOUBLE,6:A.DOUBLE,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
18: {1:A.STAND,2:A.DOUBLE,3:A.DOUBLE,4:A.DOUBLE,5:A.DOUBLE,6:A.STAND,7:A.STAND,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
19: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.DOUBLE,6:A.STAND,7:A.STAND,8:A.STAND,9:A.STAND,10:A.STAND,11:A.STAND},
20: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.STAND,8:A.STAND,9:A.STAND,10:A.STAND,11:A.STAND},
21: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.STAND,8:A.STAND,9:A.STAND,10:A.STAND,11:A.STAND}
},
pair: {
2: {1:A.HIT,2:A.HIT,3:A.SPLIT,4:A.SPLIT,5:A.SPLIT,6:A.SPLIT,7:A.SPLIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
3: {1:A.HIT,2:A.HIT,3:A.SPLIT,4:A.SPLIT,5:A.SPLIT,6:A.SPLIT,7:A.SPLIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
4: {1:A.HIT,2:A.HIT,3:A.HIT,4:A.HIT,5:A.SPLIT,6:A.SPLIT,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
5: {1:A.HIT,2:A.HIT,3:A.DOUBLE,4:A.DOUBLE,5:A.DOUBLE,6:A.DOUBLE,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
6: {1:A.HIT,2:A.SPLIT,3:A.SPLIT,4:A.SPLIT,5:A.SPLIT,6:A.SPLIT,7:A.HIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
7: {1:A.HIT,2:A.SPLIT,3:A.SPLIT,4:A.SPLIT,5:A.SPLIT,6:A.SPLIT,7:A.SPLIT,8:A.HIT,9:A.HIT,10:A.HIT,11:A.HIT},
8: {1:A.SPLIT,2:A.SPLIT,3:A.SPLIT,4:A.SPLIT,5:A.SPLIT,6:A.SPLIT,7:A.SPLIT,8:A.SPLIT,9:A.SPLIT,10:A.SPLIT,11:A.SPLIT},
9: {1:A.SPLIT,2:A.SPLIT,3:A.SPLIT,4:A.SPLIT,5:A.SPLIT,6:A.STAND,7:A.SPLIT,8:A.SPLIT,9:A.SPLIT,10:A.STAND,11:A.STAND},
10: {1:A.STAND,2:A.STAND,3:A.STAND,4:A.STAND,5:A.STAND,6:A.STAND,7:A.STAND,8:A.STAND,9:A.STAND,10:A.STAND,11:A.STAND},
11: {1:A.SPLIT,2:A.SPLIT,3:A.SPLIT,4:A.SPLIT,5:A.SPLIT,6:A.SPLIT,7:A.SPLIT,8:A.SPLIT,9:A.SPLIT,10:A.SPLIT,11:A.SPLIT}
}
};
// ================= Manual Test Function =================
function manualTest() {
log("=== MANUAL TEST START ===", 'info');
// Test button detection
const hitArea = document.querySelector('area[data-step="hit"]');
const standArea = document.querySelector('area[data-step="stand"]');
log(`HIT area found: ${!!hitArea}, disabled: ${hitArea?.classList.contains('disabled')}`, 'info');
log(`STAND area found: ${!!standArea}, disabled: ${standArea?.classList.contains('disabled')}`, 'info');
// Test game state
const gameInProgress = isGameInProgress();
const canTakeAction = canTakeAction();
log(`Game in progress: ${gameInProgress}`, 'info');
log(`Can take action: ${canTakeAction}`, 'info');
// Test hand detection
const playerHand = getHandInfo('.player-cards');
const dealerCardEl = document.querySelector('.dealer-cards div[class*="card-"]:not(.card-back)');
const dealerCardValue = getCardValue(dealerCardEl);
log(`Player hand: ${playerHand.total} (${playerHand.cards.join(',')}), Dealer: ${dealerCardValue}`, 'info');
// Try to execute action manually
if (playerHand.total > 0 && dealerCardValue > 0) {
const decision = getDecision(dealerCardValue, playerHand);
log(`Manual decision: ${decision}`, 'info');
// Force execute the action
log("Attempting to execute action manually...", 'info');
executeAction(decision);
}
log("=== MANUAL TEST END ===", 'info');
}
// Expose manual test to global scope for console access
globalWindow.manualBlackjackTest = manualTest;
function getCardValue(cardEl) {
if (!cardEl) return 0;
const match = cardEl.className.match(/card-\w+-(\w+)/);
if (!match || !match[1]) return 0;
const rank = match[1];
if (['J','Q','K'].includes(rank)) return 10;
if (rank==='A') return 11;
return parseInt(rank,10);
}
function getHandInfo(containerSelector) {
const cardsEls=document.querySelectorAll(`${containerSelector} div[class*="card-"]:not(.card-back)`);
log(`Card detection for ${containerSelector}: found ${cardsEls.length} cards`, 'debug');
cardsEls.forEach((card, i) => log(`Card ${i}: ${card.className}`, 'debug'));
const cards=Array.from(cardsEls).map(getCardValue);
let sum=cards.reduce((a,b)=>a+b,0);
let aces=cards.filter(v=>v===11).length;
while(sum>21 && aces-->0) sum-=10;
const isPair=cards.length===2 && cards[0]===cards[1];
return {cards,total:sum,isSoft:aces>0 && sum<=21,isPair};
}
function getDecision(dealerCardValue,playerHand){
const dealerIndex=dealerCardValue===11?1:dealerCardValue;
if(playerHand.isPair){
const pairValue=playerHand.cards[0]===11?11:playerHand.cards[0];
return strategy.pair[pairValue]?.[dealerIndex]||A.HIT;
}
if(playerHand.isSoft){
return strategy.soft[playerHand.total]?.[dealerIndex]||A.STAND;
}
return strategy.hard[playerHand.total]?.[dealerIndex]||A.STAND;
}
function getCurrentTokens() {
const tokenEl = document.querySelector('.bj-tokens');
return tokenEl ? parseInt(tokenEl.textContent) : 0;
}
/*function isGameInProgress() {
const dealerCards = document.querySelectorAll('.dealer-cards div[class*="card-"]');
const playerCards = document.querySelectorAll('.player-cards div[class*="card-"]');
return dealerCards.length > 0 || playerCards.length > 0;
}*/
function canOnlyStand() {
// Check if only STAND is available (HIT is disabled)
const hitArea = document.querySelector('area[data-step="hit"]');
const standArea = document.querySelector('area[data-step="stand"]');
const hitBtn = document.querySelector('a[data-step="hit"]');
const standBtn = document.querySelector('a[data-step="stand"]');
const hitDisabled = (hitArea && hitArea.classList.contains('disabled')) || (hitBtn && hitBtn.classList.contains('disabled'));
const standEnabled = (standArea && !standArea.classList.contains('disabled')) || (standBtn && !standBtn.classList.contains('disabled'));
return hitDisabled && standEnabled;
}
function canTakeAction() {
// Check both desktop (area) and mobile (a) buttons
const hitBtn = document.querySelector('area[data-step="hit"]:not(.disabled), a[data-step="hit"]:not(.disabled)');
const standBtn = document.querySelector('area[data-step="stand"]:not(.disabled), a[data-step="stand"]:not(.disabled)');
return hitBtn || standBtn;
}
function isGameInProgress() {
const dealerCards = document.querySelectorAll('.dealer-cards div[class*="card-"]');
const playerCards = document.querySelectorAll('.player-cards div[class*="card-"]');
const tableCardsVisible = document.querySelector('.table-cards.bj-show');
return tableCardsVisible && (dealerCards.length > 0 || playerCards.length > 0);
}
function isGameWaitingForNewBet() {
const newBetWrap = document.querySelector('.new-bet-wrap.bj-show');
const playBtn = document.querySelector('a[data-step="startGame"]:not(.disabled)');
return newBetWrap && playBtn;
}
function isGameShowingResult() {
const winLoseWrap = document.querySelector('.win-lose-wrap.bj-show');
const continueBtn = document.querySelector('.continue');
return winLoseWrap && continueBtn;
}
function isConfirmationDialogOpen() {
// Check if the game status wrap is actually visible and has confirmation classes
const gameStatusWrap = document.querySelector('.game-status-wrap');
if (!gameStatusWrap) return false;
// Check if the wrapper has l-confirm or r-confirm classes (indicates active confirmation)
const hasConfirmClass = gameStatusWrap.classList.contains('l-confirm') || gameStatusWrap.classList.contains('r-confirm');
if (!hasConfirmClass) return false;
// Check if the wrapper is actually visible
const style = window.getComputedStyle(gameStatusWrap);
const isVisible = style.display !== 'none' && style.visibility !== 'hidden';
if (!isVisible) return false;
// Check if there's an active confirmation text with .act class
const activeConfirmText = gameStatusWrap.querySelector('.txt.act');
if (!activeConfirmText) return false;
// Ensure YES button exists and is clickable
const yesBtn = document.querySelector('.confirm-action.yes');
if (!yesBtn) return false;
log(`Confirmation dialog is truly active: ${activeConfirmText.className}`, 'debug');
return true;
}
function clickConfirmYes() {
const now = Date.now();
if (now - lastConfirmationTime < 2000) {
// Don't click again if we just clicked within last 2 seconds
return false;
}
const yesBtn = document.querySelector('.confirm-action.yes');
if (yesBtn) {
yesBtn.click();
lastConfirmationTime = now;
log("Clicked YES to confirm action", 'info');
return true;
}
return false;
}
function clickContinue() {
const continueBtn = document.querySelector('.continue');
if (continueBtn) {
continueBtn.click();
log("Clicked CONTINUE button", 'info');
return true;
}
return false;
}
function getGameResult() {
const resultEl = document.querySelector('.bj-wonState');
return resultEl ? resultEl.textContent.trim() : '';
}
// ================= Bot Actions =================
function clickElement(selector) {
const element = document.querySelector(selector);
if (element && !element.classList.contains('disabled')) {
element.click();
return true;
}
return false;
}
function clickAction(action) {
// Try desktop image map buttons first
let selector = '';
switch(action) {
case 'hit':
selector = 'area[data-step="hit"]:not(.disabled)';
break;
case 'stand':
selector = 'area[data-step="stand"]:not(.disabled)';
break;
case 'doubleDown':
selector = 'area[data-step="doubleDown"]:not(.disabled)';
break;
case 'split':
selector = 'area[data-step="split"]:not(.disabled)';
break;
}
if (clickElement(selector)) return true;
// Fallback to mobile buttons
switch(action) {
case 'hit':
return clickElement('a[data-step="hit"]:not(.disabled)');
case 'stand':
return clickElement('a[data-step="stand"]:not(.disabled)');
case 'doubleDown':
return clickElement('a[data-step="doubleDown"]:not(.disabled)');
case 'split':
return clickElement('a[data-step="split"]:not(.disabled)');
}
return false;
}
function setBetAmount(amount) {
const betInput = document.querySelector('.bet.input-money[type="text"]');
if (betInput) {
betInput.value = amount.toString();
betInput.dispatchEvent(new Event('input', { bubbles: true }));
betInput.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
return false;
}
function startNewGame() {
if (isGameWaitingForNewBet()) {
setBetAmount(betAmount);
setTimeout(() => {
if (clickElement('a[data-step="startGame"]:not(.disabled)')) {
log(`Started new game with bet: $${betAmount}`, 'info');
return true;
}
}, 500);
}
return false;
}
function executeAction(action) {
let clicked = false;
let actionKey = '';
switch(action) {
case A.HIT:
actionKey = 'hit';
break;
case A.STAND:
actionKey = 'stand';
break;
case A.DOUBLE:
// Check if double is available, otherwise hit
if (document.querySelector('area[data-step="doubleDown"]:not(.disabled), a[data-step="doubleDown"]:not(.disabled)')) {
actionKey = 'doubleDown';
} else {
actionKey = 'hit';
action = A.HIT; // Update action for logging
}
break;
case A.SPLIT:
// Check if split is available, otherwise hit
if (document.querySelector('area[data-step="split"]:not(.disabled), a[data-step="split"]:not(.disabled)')) {
actionKey = 'split';
} else {
actionKey = 'hit';
action = A.HIT; // Update action for logging
}
break;
}
if (actionKey) {
clicked = clickAction(actionKey);
}
if (clicked) {
log(`Bot executed: ${action}`, 'success');
} else {
log(`Bot failed to execute: ${action} (button not available)`, 'warning');
}
return clicked;
}
function updateBotStats() {
const currentTokens = getCurrentTokens();
if (botStats.startingTokens === 0) {
botStats.startingTokens = currentTokens;
}
botStats.currentTokens = currentTokens;
// Check for game results on the result screen
if (isGameShowingResult()) {
const resultEl = document.querySelector('.bj-wonState');
const msgEl = document.querySelector('.wl-msg span');
if (resultEl && msgEl) {
const result = resultEl.textContent.trim();
const msg = msgEl.textContent.trim();
log(`Game result: ${msg} - ${result}`, 'info');
botStats.totalHands++;
if (msg.includes('WIN') || msg.includes('BLACKJACK') || result.includes('won')) {
botStats.wins++;
currentLossStreak = 0;
} else if (msg.includes('BUST') || msg.includes('LOSE') || result.includes('lost') || result.includes('higher than 21')) {
botStats.losses++;
currentLossStreak++;
}
}
}
}
function shouldStopBot() {
const currentTokens = getCurrentTokens();
if (currentTokens <= stopOnTokens) {
log(`Bot stopped: Tokens reached minimum (${currentTokens})`, 'warning');
return true;
}
if (currentLossStreak >= maxLossStreak) {
log(`Bot stopped: Max loss streak reached (${currentLossStreak})`, 'warning');
return true;
}
return false;
}
// ================= Bot Logic =================
function botTick() {
if (!botEnabled || !active) return;
updateBotStats();
if (shouldStopBot()) {
botEnabled = false;
updateBotDisplay();
return;
}
const now = Date.now();
// Don't take any action if we're still waiting from the last action
if (waitingForAction && (now - lastActionTime < autoPlayDelay)) {
return;
}
waitingForAction = false;
// Debug: Log current game state (but less frequently to reduce spam)
const gameStates = {
confirmationDialog: now > ignoreConfirmationsUntil && isConfirmationDialogOpen(),
resultScreen: isGameShowingResult(),
newBetScreen: isGameWaitingForNewBet(),
gameInProgress: isGameInProgress(),
canTakeAction: canTakeAction()
};
// Check if there's a confirmation dialog open (with safety checks)
if (gameStates.confirmationDialog) {
log("Bot detected active confirmation dialog, clicking YES...", 'info');
waitingForAction = true;
lastActionTime = now;
setTimeout(() => {
if (clickConfirmYes()) {
ignoreConfirmationsUntil = Date.now() + 5000; // Ignore confirmations for 5 seconds
}
}, 1000);
return;
}
// Check if game is showing results and needs continue click
if (gameStates.resultScreen) {
log("Bot detected result screen, clicking continue...", 'info');
waitingForAction = true;
lastActionTime = now;
setTimeout(() => {
clickContinue();
}, autoPlayDelay);
return;
}
// Check if we need to start a new game
if (gameStates.newBetScreen) {
log("Bot detected new bet screen, starting game...", 'info');
waitingForAction = true;
lastActionTime = now;
setTimeout(() => {
startNewGame();
}, autoPlayDelay);
return;
}
// Check if we can take an action in current game
if (gameStates.gameInProgress) {
const dealerCardEl = document.querySelector('.dealer-cards div[class*="card-"]');
const dealerCardValue = getCardValue(dealerCardEl);
const playerHand = getHandInfo('.player-cards');
if (dealerCardValue && playerHand.cards.length >= 2) {
// Check if only STAND is available (common when player has 21 or game is ending)
if (canOnlyStand()) {
log(`Only STAND available. Player total: ${playerHand.total}. Standing to end game.`, 'info');
waitingForAction = true;
lastActionTime = now;
setTimeout(() => {
executeAction(A.STAND);
}, autoPlayDelay);
} else if (gameStates.canTakeAction) {
// Both HIT and STAND available - use strategy
if (playerHand.total === 21) {
// Player has 21 - should stand to win
log(`Player has 21! Standing to win.`, 'info');
waitingForAction = true;
lastActionTime = now;
setTimeout(() => {
executeAction(A.STAND);
}, autoPlayDelay);
} else if (playerHand.total < 21) {
// Normal play - use strategy
const decision = getDecision(dealerCardValue, playerHand);
log(`Strategy decision: ${decision} (Player: ${playerHand.total} vs Dealer: ${dealerCardValue})`, 'info');
waitingForAction = true;
lastActionTime = now;
setTimeout(() => {
executeAction(decision);
}, autoPlayDelay);
} else {
// Player busted (over 21) - usually auto-ends but stand if available
log(`Player busted (${playerHand.total}), waiting for game to end`, 'debug');
}
} else {
// No buttons available - game is likely ending
log(`Game ending - no actions available. Player: ${playerHand.total}`, 'debug');
}
}
}
}
// ================= UI =================
function createInlineDisplay() {
if (document.getElementById('bj-helper-inline')) return;
const gameWrap = document.querySelector('.blackjack-wrap');
if (!gameWrap) return;
const container = document.createElement('div');
container.id = 'bj-helper-inline';
container.style.cssText = `
margin-bottom:8px;
padding:12px;
background:rgba(55,178,77,0.05);
border:1px solid #37b24d;
border-radius:8px;
font-family:monospace;
font-size:14px;
color:#495057;
`;
container.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<span id='bj-helper-advice-text' style="font-weight:bold;">---</span>
<span id='bj-helper-hand-total'>Waiting for hand</span>
</div>
<div id="bj-bot-status" style="display:none;font-size:12px;color:#666;border-top:1px solid #ddd;padding-top:8px;margin-top:8px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;">
<div>Hands: <span id="bot-hands">0</span></div>
<div>Wins: <span id="bot-wins">0</span></div>
<div>Losses: <span id="bot-losses">0</span></div>
</div>
<div style="margin-top:4px;">
Tokens: <span id="bot-tokens">0</span> | Streak: <span id="bot-streak">0</span>
</div>
</div>
`;
gameWrap.parentNode.insertBefore(container, gameWrap);
}
function updateInlineDisplay(decision = '---', total = 0) {
const container = document.getElementById('bj-helper-inline');
if (!container) return;
const adviceText = document.getElementById('bj-helper-advice-text');
const handTotal = document.getElementById('bj-helper-hand-total');
adviceText.textContent = botEnabled ? `🤖 ${decision}` : decision;
handTotal.textContent = total > 0 ? `Hand: ${total}` : 'Waiting for hand';
const colors = { Hit: '#ff6b6b', Stand: '#51cf66', Double: '#fcc419', Split: '#748ffc', '---': '#adb5bd' };
adviceText.style.color = colors[decision] || '#212529';
}
function updateBotDisplay() {
const statusDiv = document.getElementById('bj-bot-status');
if (!statusDiv) return;
statusDiv.style.display = botEnabled ? 'block' : 'none';
if (botEnabled) {
document.getElementById('bot-hands').textContent = botStats.totalHands;
document.getElementById('bot-wins').textContent = botStats.wins;
document.getElementById('bot-losses').textContent = botStats.losses;
document.getElementById('bot-tokens').textContent = botStats.currentTokens;
document.getElementById('bot-streak').textContent = currentLossStreak;
}
}
// ================= Main Observer =================
function mainObserverCallback() {
const dealerCardEl = document.querySelector('.dealer-cards div[class*="card-"]');
const dealerCardValue = getCardValue(dealerCardEl);
const playerHand = getHandInfo('.player-cards');
const canTakeAction = document.querySelector('a[data-step="hit"]:not(.disabled), a[data-step="stand"]:not(.disabled)');
if (!dealerCardValue || playerHand.cards.length < 2 || !canTakeAction || playerHand.total >= 21) {
updateInlineDisplay('---');
} else {
const decision = getDecision(dealerCardValue, playerHand);
updateInlineDisplay(decision, playerHand.total);
}
updateBotDisplay();
// Run bot logic
if (botEnabled) {
botTick();
}
}
// ================= Lifecycle =================
function start() {
if (active) return;
log("Starting module", "success");
const gameContainer = document.querySelector('.blackjack, .blackjack-wrap');
if (!gameContainer) {
log("Blackjack game not found", "warning");
return;
}
createInlineDisplay();
observer = new MutationObserver(mainObserverCallback);
observer.observe(gameContainer, { childList: true, subtree: true });
mainObserverCallback();
active = true;
}
function stop() {
if (!active) return;
log("Stopping module", "warning");
botEnabled = false;
if (observer) observer.disconnect();
const overlay = document.getElementById('bj-helper-inline');
if (overlay) overlay.remove();
active = false;
}
function toggleBot() {
botEnabled = !botEnabled;
if (botEnabled) {
// Reset stats when starting bot
botStats = { wins: 0, losses: 0, totalHands: 0, startingTokens: getCurrentTokens(), currentTokens: getCurrentTokens() };
currentLossStreak = 0;
log("Bot enabled", "success");
} else {
log("Bot disabled", "warning");
}
updateBotDisplay();
}
function isActive() { return active; }
// ================= Menu =================
const menuSection = `
<h4 style='margin:0 0 8px 0;color:#f59f00;'>${MODULE_NAME}</h4>
<label style='display:flex;align-items:center;cursor:pointer;margin-bottom:8px;'>
<input type='checkbox' id='toggle-${MODULE_NAME}'>
<span style='margin-left:6px;'>Enable ${MODULE_NAME}</span>
</label>
<div id="bot-controls" style="border-top:1px solid #444;padding-top:8px;margin-top:8px;">
<label style='display:flex;align-items:center;cursor:pointer;margin-bottom:8px;'>
<input type='checkbox' id='toggle-bot-${MODULE_NAME}'>
<span style='margin-left:6px;color:#ff6b6b;font-weight:bold;'>🤖 Enable Auto-Play Bot</span>
</label>
<div style="margin-left:20px;font-size:11px;color:#ccc;">
<div style="margin-bottom:4px;">
<label>Bet Amount: $</label>
<input type="number" id="bot-bet-amount" value="${betAmount}" style="width:80px;background:#333;color:white;border:1px solid #555;padding:2px;">
</div>
<div style="margin-bottom:4px;">
<label>Play Delay: </label>
<input type="number" id="bot-delay" value="${autoPlayDelay}" style="width:60px;background:#333;color:white;border:1px solid #555;padding:2px;">ms
</div>
<div style="margin-bottom:4px;">
<label>Max Loss Streak: </label>
<input type="number" id="bot-max-losses" value="${maxLossStreak}" style="width:40px;background:#333;color:white;border:1px solid #555;padding:2px;">
</div>
<div style="margin-bottom:4px;">
<label>Stop at Tokens: </label>
<input type="number" id="bot-stop-tokens" value="${stopOnTokens}" style="width:40px;background:#333;color:white;border:1px solid #555;padding:2px;">
</div>
</div>
</div>
`;
function setupMenuEvents() {
const moduleToggle = document.getElementById(`toggle-${MODULE_NAME}`);
const botToggle = document.getElementById(`toggle-bot-${MODULE_NAME}`);
const betAmountInput = document.getElementById('bot-bet-amount');
const delayInput = document.getElementById('bot-delay');
const maxLossesInput = document.getElementById('bot-max-losses');
const stopTokensInput = document.getElementById('bot-stop-tokens');
if (moduleToggle) {
moduleToggle.checked = active;
moduleToggle.onchange = () => moduleToggle.checked ? start() : stop();
}
if (botToggle) {
botToggle.checked = botEnabled;
botToggle.onchange = () => toggleBot();
}
if (betAmountInput) {
betAmountInput.onchange = () => {
betAmount = parseInt(betAmountInput.value) || 1000;
log(`Bot bet amount set to: $${betAmount}`, 'info');
};
}
if (delayInput) {
delayInput.onchange = () => {
autoPlayDelay = parseInt(delayInput.value) || 2000;
log(`Bot delay set to: ${autoPlayDelay}ms`, 'info');
};
}
if (maxLossesInput) {
maxLossesInput.onchange = () => {
maxLossStreak = parseInt(maxLossesInput.value) || 5;
log(`Max loss streak set to: ${maxLossStreak}`, 'info');
};
}
if (stopTokensInput) {
stopTokensInput.onchange = () => {
stopOnTokens = parseInt(stopTokensInput.value) || 5;
log(`Stop tokens set to: ${stopOnTokens}`, 'info');
};
}
}
// ================= Registration =================
function registerModule() {
try {
globalWindow.TornFramework.registerModule({
name: MODULE_NAME,
version: "4.0",
description: "Blackjack strategy helper with auto-play bot",
menuSection,
initialize: () => { start(); setupMenuEvents(); },
cleanup: stop,
isActive
});
log("Module registered with TornFramework", "success");
} catch (err) {
console.error(`[${MODULE_NAME}] Failed to register:`, err);
log("Running standalone fallback", "warning");
start();
}
}
if (globalWindow.TornFramework?.initialized) {
registerModule();
} else {
const wait = setInterval(() => {
if (globalWindow.TornFramework?.initialized) {
clearInterval(wait);
registerModule();
}
}, 500);
}
})();