// ==UserScript==
// @name Stake Keno Bot
// @namespace http://tampermonkey.net/
// @version 3.3.3
// @description Advanced Keno Bot for Stake.us and Stake.com with pattern shifting/animation, resizable UI, pattern creator, risk selection, seed changing, and advanced strategy options.
// @author LTC: ltc1qvpmsjyn6y7vk080uhje8v63mvty4adp7ewk20c (Original by Author, Bot UI by Gemini)
// @license MIT
// @match https://stake.us/casino/games/keno
// @match https://stake.com/casino/games/keno
// @grant GM_addStyle
// @grant unsafeWindow
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION & STATE ---
const botState = {
running: false,
betCount: 0,
totalProfit: 0,
winStreak: 0,
lossStreak: 0,
lastBetWasWin: null,
lastMultiplier: 0,
autoBetLoopInterval: null,
lastPickedNumbers: [], // Holds the last set of picked numbers to avoid re-picking
isLocked: false,
isBotInitialized: false,
logEntries: [],
isPickingElement: false,
pendingAction: null,
storedUsername: '',
storedPasswordHash: '',
strategyFlags: {}, // For stateful conditions like 'firstStreakOf'
persistentTriggerStates: {}, // For tracking profit/time based one-time actions
apiBlockStartTime: 0, // To track API cooldown
};
const kenoState = {
type: null, // 'bar', 'bird', 'random', 'custom', or 'user'
position: null, // 'left', 'middle', 'right'
flip: false,
customCount: 3,
userPatternName: null, // For 'user' type
riskLevel: 'medium', // Default risk
animation: { // For pattern shifting
enabled: false,
type: 'none',
patternName: null,
currentFrame: 0,
currentAnchor: { row: 0, col: 0 },
currentVelocity: { row: 1, col: 1 },
metadata: null, // Will hold { offsets, height, width, validAnchorRows, validAnchorCols }
customFrames: [] // Will hold arrays of tile numbers
}
};
// For pattern creator modal dragging state
const patternCreatorState = {
isDragging: false,
dragStartTile: null,
dragStartCoords: null,
currentTiles: [],
currentMetadata: null,
};
const patterns = {
bar: { left: [1, 2, 9, 10, 17, 18, 25, 26, 33, 34], middle: [4, 5, 12, 13, 20, 21, 28, 29, 36, 37], right: [7, 8, 15, 16, 23, 24, 31, 32, 39, 40] },
bar_flip: { left: [2, 3, 10, 11, 18, 19, 26, 27, 34, 35], middle: [4, 5, 12, 13, 20, 21, 28, 29, 36, 37], right: [6, 7, 14, 15, 22, 23, 30, 31, 38, 39] },
bird: { left: [33, 34, 35, 36, 37, 26, 27, 28, 19, 11], middle: [12, 20, 27, 28, 29, 34, 35, 36, 37, 38], right: [36, 37, 38, 39, 40, 29, 30, 31, 22, 14] },
bird_flip: { left: [1, 9, 10, 17, 18, 19, 20, 25, 26, 33], middle: [2, 3, 4, 5, 6, 11, 12, 13, 20, 28], right: [21, 22, 23, 24, 15, 16, 8, 31, 32, 40] },
};
let userPatterns = {}; // Will store { name: { tiles: [], metadata: {}, animation: {} } }
let advancedStrategies = {};
const logFilters = {
system: true,
manual: true,
auto: true,
strategy: true,
};
let CONFIG = {
authToken: '', // Added for seed change
seedChangeEndpoint: '/_api/graphql', // Added for seed change
betButton: '[data-testid="bet-button"]',
clearButton: '[data-testid="clear-table-button"]',
tile: '[data-testid^="tile-"], .tile.svelte-vebeey', // MODIFIED: More general selector for tiles
amountInput: '[data-testid="input-game-amount"]',
riskSelect: '[data-testid="risk-select"]', // Added risk selector
pastBetsContainer: '.past-bets',
injectControlsToPage: false,
uiScale: 100,
autoGames: 0, // Number of games for auto-bet
};
const CONFIG_FRIENDLY_NAMES = {
authToken: "Auth Token (Auto-detected)", // Added
seedChangeEndpoint: "API Endpoint", // Added
betButton: "Bet Button",
clearButton: "Clear Button",
tile: "Keno Tile",
amountInput: "Amount Input",
riskSelect: "Risk Select Dropdown", // Added
pastBetsContainer: "Past Bets List",
injectControlsToPage: "Inject Manual Controls to Page",
uiScale: "UI Scale (%)",
autoGames: "Games to Run (0=inf)",
};
// --- UI (HTML & CSS) ---
const botHtml = `
<div id="bot-notification-container"></div>
<div id="keno-bot-window" class="bot-window">
<!-- Resizers -->
<div class="resizer resizer-t"></div><div class="resizer resizer-r"></div><div class="resizer resizer-b"></div><div class="resizer resizer-l"></div>
<div class="resizer resizer-tl"></div><div class="resizer resizer-tr"></div><div class="resizer resizer-br"></div><div class="resizer resizer-bl"></div>
<!-- Header -->
<div id="bot-header" class="bot-header">
<span>Stake Keno Bot <span id="bot-status-indicator"></span> <span id="profit-loss-display"></span></span>
<div class="window-controls">
<button id="minimize-btn" class="window-btn" title="Minimize">—</button>
<button id="close-btn" class="window-btn" title="Close">×</button>
</div>
</div>
<!-- Main Content -->
<div id="bot-main-content" class="bot-content">
<div class="bot-tabs">
<button class="bot-tab-btn active" data-tab="manual">Manual</button>
<button class="bot-tab-btn" data-tab="auto">Auto</button>
<button class="bot-tab-btn" data-tab="patterns">Patterns</button>
<button class="bot-tab-btn" data-tab="advanced">Advanced</button>
<button class="bot-tab-btn" data-tab="log">Log</button>
<button class="bot-tab-btn" data-tab="config">Config</button>
<button class="bot-tab-btn" data-tab="admin">Admin</button>
</div>
<!-- Manual Tab -->
<div id="tab-manual" class="bot-tab-content active">
<!-- Compacted Groups -->
<div class="compact-grid-2">
<div class="bot-input-group">
<label>Pattern Type</label>
<div class="btn-grid-3">
<button id="manual-btn-bar" data-category="type" data-value="bar" class="bot-btn bot-btn-secondary">Bar</button>
<button id="manual-btn-bird" data-category="type" data-value="bird" class="bot-btn bot-btn-secondary">Bird</button>
<button id="manual-btn-random" data-category="type" data-value="random" class="bot-btn bot-btn-secondary">Random</button>
</div>
</div>
<div class="bot-input-group">
<label>Position / Flip</label>
<div class="btn-grid-4">
<button id="manual-btn-flip" data-category="flip" class="bot-btn bot-btn-secondary" title="Flip">Flip</button>
<button id="manual-btn-left" data-category="position" data-value="left" class="bot-btn bot-btn-secondary" title="Left">L</button>
<button id="manual-btn-middle" data-category="position" data-value="middle" class="bot-btn bot-btn-secondary" title="Middle">M</button>
<button id="manual-btn-right" data-category="position" data-value="right" class="bot-btn bot-btn-secondary" title="Right">R</button>
</div>
</div>
<div class="bot-input-group">
<label>Custom Random</label>
<div class="flex-container justify-between">
<button id="manual-btn-custom" data-category="type" data-value="custom" class="bot-btn bot-btn-secondary" style="flex-grow: 1;">Custom</button>
<input type="number" id="keno-custom-count-input" class="bot-input" min="1" max="10" value="3" title="Number of random tiles" style="width: 60px; text-align: center; padding: 8px 4px;">
</div>
</div>
<div class="bot-input-group">
<label>Risk Level</label>
<select id="manual-risk-select" class="bot-input" style="padding: 8px 4px;">
<option value="classic">Classic</option>
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<div id="manual-user-patterns" class="bot-input-group">
<label>User Patterns (<span id="manual-pattern-animation-status" style="font-weight: bold; color: var(--accent-yellow);"></span>)</label>
<div id="manual-user-patterns-container" class="btn-grid-3">
<!-- User patterns will be injected here -->
</div>
</div>
<hr>
<!-- New Manual Strategy/Bet Section -->
<div class="bot-input-group">
<label>Manual Strategy Step</label>
<select id="manual-strategy-select" class="bot-input">
<option value="none">None (Use Manual Selection)</option>
<!-- Strategies will be populated here -->
</select>
<p class="strategy-desc" style="margin-top: 8px; margin-bottom: 8px;">
Select a strategy to apply its logic on the next manual bet, or select "None" to use the pattern chosen above.
</p>
</div>
<div class="btn-grid-2">
<button id="manual-bet-btn" class="bot-btn bot-btn-primary" title="Place a single bet (B)">Manual Bet (B)</button>
<button id="manual-change-seed-btn" class="bot-btn bot-btn-secondary" title="Change Client Seed via API">Change Seed</button>
</div>
</div>
<!-- Auto Tab -->
<div id="tab-auto" class="bot-tab-content">
<div class="bot-input-group">
<label>Games to Run (0 for infinite)</label>
<input type="number" id="keno-games-input" class="bot-input" value="0" min="0">
</div>
<div class="bot-input-group">
<label>Strategy</label>
<select id="auto-strategy-select" class="bot-input">
<option value="none">None (Use Manual Selection)</option>
</select>
</div>
<p class="strategy-desc">
If "None" is selected, the bot will continuously run the pattern and risk level currently selected on the "Manual" tab.
If a user pattern with animation is selected, it will animate.
If a strategy is selected, it will override the manual selection.
</p>
<button id="start-stop-btn" class="bot-btn bot-btn-primary" title="Starts or stops the bot (S)">Start Auto (S)</button>
</div>
<!-- Patterns Tab -->
<div id="tab-patterns" class="bot-tab-content">
<div class="bot-input-group">
<label>Your Saved Patterns</label>
<select id="user-pattern-list" class="bot-input" size="8"></select>
</div>
<div class="btn-grid-3">
<button id="create-pattern-btn" class="bot-btn bot-btn-secondary">Create New</button>
<button id="edit-pattern-btn" class="bot-btn bot-btn-secondary">Edit Selected</button>
<button id="delete-pattern-btn" class="bot-btn bot-btn-danger">Delete</button>
</div>
</div>
<!-- Advanced Tab -->
<div id="tab-advanced" class="bot-tab-content">
<div class="bot-input-group">
<label>Your Saved Strategies</label>
<select id="advanced-strategy-list" class="bot-input" size="8"></select>
</div>
<div class="btn-grid-2">
<button id="create-strategy-btn" class="bot-btn bot-btn-secondary">Create New</button>
<button id="edit-strategy-btn" class="bot-btn bot-btn-secondary">Edit Selected</button>
<button id="import-strategy-btn" class="bot-btn bot-btn-secondary">Import</button>
<button id="export-strategy-btn" class="bot-btn bot-btn-secondary">Export Selected</button>
</div>
<button id="delete-strategy-btn" class="bot-btn bot-btn-danger">Delete Selected</button>
</div>
<!-- Log Tab -->
<div id="tab-log" class="bot-tab-content">
<div id="log-filters">
<label><input type="checkbox" data-filter="system" checked> System</label>
<label><input type="checkbox" data-filter="manual" checked> Manual</label>
<label><input type="checkbox" data-filter="auto" checked> Auto</label>
<label><input type="checkbox" data-filter="strategy" checked> Strategy</label>
</div>
<div id="log-container"></div>
<div class="btn-grid-2">
<button id="reset-stats-btn" class="bot-btn bot-btn-danger">Reset Stats</button>
<button id="save-log-btn" class="bot-btn bot-btn-secondary">Save Log to File</button>
</div>
</div>
<!-- Config Tab -->
<div id="tab-config" class="bot-tab-content">
<p class="strategy-desc">Configure UI element selectors and bot behavior.</p>
<div id="config-container"></div>
<button id="save-config-btn" class="bot-btn bot-btn-primary">Save Config</button>
<button id="reset-config-btn" class="bot-btn bot-btn-danger">Reset to Defaults</button>
</div>
<!-- Admin Tab -->
<div id="tab-admin" class="bot-tab-content">
<p class="strategy-desc">Set a password to lock bot settings. When locked, only the Manual and Admin tabs are usable.</p>
<div class="bot-input-group"><label>Username</label><input type="text" id="admin-username" class="bot-input" autocomplete="username"></div>
<div class="bot-input-group"><label>New Password (leave blank to keep current)</label><input type="password" id="admin-password" class="bot-input" autocomplete="new-password"></div>
<button id="save-credentials-btn" class="bot-btn bot-btn-secondary">Save Credentials</button>
<hr>
<button id="lock-unlock-btn" class="bot-btn bot-btn-primary">Lock</button>
<p id="admin-status" class="strategy-desc" style="margin-top: 15px; text-align: center;"></p>
</div>
</div>
</div>
<!-- Minimized Bar -->
<div id="keno-bot-minimized-bar" style="display: none;">
<span>Stake Keno Bot</span>
<div class="window-controls">
<button id="maximize-btn" class="window-btn" title="Maximize">□</button>
<button id="close-minimized-btn" class="window-btn" title="Close">×</button>
</div>
</div>
<!-- Modals -->
<div id="pattern-creator-modal" class="bot-modal-backdrop" style="display: none;">
<div class="bot-modal-content" style="max-width: 700px; width: 90%;">
<h3 id="pattern-modal-title">Create New Pattern</h3>
<div class="bot-input-group">
<label>Pattern Name</label>
<input type="text" id="pattern-name" class="bot-input">
</div>
<div class="bot-input-group">
<label>Click to select up to 10 tiles. If 'Custom Animation' is on, you can drag the pattern.</label>
<div id="keno-pattern-grid">
${Array.from({ length: 40 }, (_, i) => `<div class="keno-pattern-tile" data-number="${i + 1}">${i + 1}</div>`).join('')}
</div>
</div>
<!-- Animation Settings -->
<div id="pattern-animation-settings" class="strategy-rule">
<div class="bot-input-group" style="margin-bottom: 10px;">
<label class="flex-container" style="justify-content: space-between;">
Pattern Animation
<input type="checkbox" id="pattern-animation-enable" style="width: 20px; height: 20px;">
</label>
</div>
<div id="pattern-animation-controls" style="display: none;">
<div class="bot-input-group" style="margin-bottom: 10px;">
<label>Animation Type</label>
<select id="pattern-animation-type" class="bot-input">
<option value="none">None</option>
<option value="random">Random Shift</option>
<option value="dvd">DVD Bounce</option>
<option value="leftRight">Left-Right</option>
<option value="custom">Custom (Frames)</option>
</select>
</div>
<div id="pattern-animation-frames-ui" style="display: none;">
<label>Animation Frames</label>
<div class="flex-container" style="gap: 10px; align-items: flex-start;">
<select id="pattern-animation-frames-list" class="bot-input" size="5" style="flex-grow: 1;"></select>
<div class="flex-container" style="flex-direction: column; gap: 5px; flex-shrink: 0;">
<button id="pattern-frame-add" class="bot-btn bot-btn-secondary" style="margin: 0; padding: 5px;" title="Save Current Grid as Frame">Save Frame</button>
<button id="pattern-frame-delete" class="bot-btn bot-btn-danger" style="margin: 0; padding: 5px;" title="Delete Selected Frame">Delete</button>
<button id="pattern-frame-up" class="bot-btn bot-btn-secondary" style="margin: 0; padding: 5px;" title="Move Up">↑</button>
<button id="pattern-frame-down" class="bot-btn bot-btn-secondary" style="margin: 0; padding: 5px;" title="Move Down">↓</button>
</div>
</div>
<p class="strategy-desc" style="margin-top: 8px;">Drag pattern on grid above, then 'Save Frame'.</p>
</div>
</div>
</div>
<div class="btn-grid-2">
<button id="save-pattern-btn" class="bot-btn bot-btn-primary">Save</button>
<button id="cancel-pattern-btn" class="bot-btn bot-btn-danger">Cancel</button>
</div>
</div>
</div>
<div id="strategy-modal" class="bot-modal-backdrop" style="display: none;">
<div id="strategy-modal-content" class="bot-modal-content">
<h3 id="strategy-modal-title">Create Strategy</h3>
<div class="bot-input-group"><label>Strategy Name</label><input type="text" id="strategy-name" class="bot-input"></div>
<div id="strategy-rules-container"></div>
<button id="add-rule-btn" class="bot-btn bot-btn-secondary">Add Condition Block</button>
<hr>
<p id="strategy-modal-warning" style="color: var(--accent-red); text-align: center; display: none;"></p>
<button id="save-strategy-btn" class="bot-btn bot-btn-primary">Save Strategy</button>
<button id="cancel-strategy-btn" class="bot-btn bot-btn-danger">Cancel</button>
</div>
</div>
<div id="password-prompt-modal" class="bot-modal-backdrop" style="display: none;">
<div id="password-prompt-content" class="bot-modal-content">
<h3>Password Required</h3>
<p id="password-prompt-message">Please enter the password to unlock this feature.</p>
<div class="bot-input-group"><label>Password</label><input type="password" id="prompt-password-input" class="bot-input" autocomplete="current-password"></div>
<p id="password-prompt-error" style="color: var(--accent-red); display: none;"></p>
<div class="btn-grid-2">
<button id="password-prompt-submit" class="bot-btn bot-btn-primary">Submit</button>
<button id="password-prompt-cancel" class="bot-btn bot-btn-secondary">Cancel</button>
</div>
</div>
</div>
<div id="element-picker-overlay"></div>
<div id="element-picker-tooltip">Click an element to select it. Press ESC to cancel.</div>
`;
const botCss = `
:root { --bg-primary: #213743; --bg-secondary: #2F4553; --bg-tertiary: #1A2D3A; --accent-green: #00b373; --accent-red: #e53e3e; --accent-yellow: #FFC107; --accent-blue: #3b82f6; --text-primary: #fff; --text-secondary: #b1bad3; }
.bot-window { position: fixed; top: 100px; right: 20px; width: 380px; min-width: 350px; max-width: 800px; height: 600px; min-height: 400px; max-height: 90vh; background-color: var(--bg-primary); border-radius: 8px; z-index: 9999; color: var(--text-primary); font-family: 'Inter', sans-serif; box-shadow: 0 5px 15px rgba(0,0,0,0.5); display: none; flex-direction: column; overflow: hidden; }
.bot-header { padding: 10px 15px; cursor: move; background-color: var(--bg-secondary); font-weight: 600; user-select: none; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; }
#bot-status-indicator { font-weight: normal; font-size: 12px; opacity: 0.8; margin-left: 8px; }
#profit-loss-display { font-weight: 600; font-size: 12px; margin-left: 10px; }
#profit-loss-display.profit { color: var(--accent-green); }
#profit-loss-display.loss { color: var(--accent-red); }
.window-controls { display: flex; gap: 8px; align-items: center; }
.window-btn { background: none; border: none; color: var(--text-secondary); font-size: 20px; cursor: pointer; line-height: 1; padding: 0 5px; font-family: sans-serif; display: flex; align-items: center; justify-content: center; }
.window-btn:hover { color: var(--text-primary); }
#maximize-btn { font-size: 16px; }
.bot-content { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; min-height: 0; }
.bot-tabs { display: flex; margin-bottom: 15px; background-color: var(--bg-primary); border-radius: 20px; padding: 4px; border: 1px solid var(--bg-secondary); flex-shrink: 0; overflow-x: auto; }
.bot-tab-btn { flex: 1; padding: 8px; cursor: pointer; background: transparent; border: none; color: var(--text-secondary); border-radius: 20px; font-weight: 500; transition: all 0.2s ease-in-out; font-size: 13px; white-space: nowrap; }
.bot-tab-btn.active { background-color: var(--bg-secondary); color: var(--text-primary); }
.bot-tabs.locked .bot-tab-btn:not([data-tab="manual"]):not([data-tab="admin"]) { pointer-events: none; opacity: 0.5; }
.bot-tab-content { display: none; overflow-y: auto; flex-grow: 1; flex-direction: column; padding-right: 5px; /* Add padding for scrollbar */ }
.bot-tab-content.active { display: flex; }
.bot-input-group { margin-bottom: 10px; display: flex; flex-direction: column; }
.bot-input-group label { margin-bottom: 4px; font-size: 13px; color: #b1bad3; }
.bot-input { width: 100%; padding: 8px; background-color: var(--bg-primary); border: 1px solid var(--bg-secondary); border-radius: 4px; color: #fff; box-sizing: border-box; }
.bot-input:disabled { background-color: #2a414f; opacity: 0.7; }
select.bot-input { appearance: none; background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23b1bad3' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right 8px center; background-size: 1em; padding-right: 2.5em; }
select.bot-input[size] { background-image: none; padding-right: 8px; }
.bot-btn { width: 100%; padding: 10px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 600; margin-top: 8px; transition: all 0.2s ease; }
.bot-btn:disabled { background-color: #4A5568; cursor: not-allowed; opacity: 0.7; }
.bot-btn-primary { background-color: var(--accent-green); color: #fff; }
.bot-btn-danger { background-color: var(--accent-red); color: #fff; }
.bot-btn-secondary { background-color: var(--bg-secondary); color: #fff; }
.bot-btn-secondary.active { background-color: var(--accent-blue); }
.bot-btn:hover:not(:disabled) { opacity: 0.8; }
.strategy-desc { font-size: 12px; color: #b1bad3; background-color: var(--bg-secondary); padding: 8px; border-radius: 4px; margin: 5px 0 15px 0; }
#config-container { background-color: var(--bg-primary); border-radius: 4px; padding: 8px; font-size: 12px; line-height: 1.5; }
#log-container { flex-grow: 1; overflow-y: auto; padding-right: 5px; background-color: var(--bg-primary); border: 1px solid var(--bg-secondary); border-radius: 4px; padding: 8px; font-size: 12px; line-height: 1.5; min-height: 100px; }
#log-container p { margin: 0 0 5px 0; padding: 2px 4px; border-radius: 3px; }
#log-container p.log-manual { color: var(--accent-green); }
#log-container p.log-auto { color: var(--accent-red); }
#log-container p.log-strategy { color: var(--accent-yellow); }
#log-container p b { font-weight: 900; }
#log-filters { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 10px; background-color: var(--bg-secondary); padding: 8px; border-radius: 4px; }
#log-filters label { display: flex; align-items: center; gap: 5px; font-size: 12px; cursor: pointer; }
#keno-bot-minimized-bar { position: fixed; top: 100px; right: 20px; background-color: var(--bg-secondary); border-radius: 8px; z-index: 9999; color: #fff; font-family: 'Inter', sans-serif; box-shadow: 0 5px 15px rgba(0,0,0,0.5); display: none; justify-content: space-between; align-items: center; padding: 8px 12px; cursor: move; }
#keno-bot-minimized-bar span { font-weight: 600; user-select: none; }
.config-group { display: flex; align-items: center; gap: 5px; margin-bottom: 8px; }
.config-group label { font-size: 12px; color: #b1bad3; flex-basis: 140px; flex-shrink: 0; display: flex; align-items: center; }
.config-group .input-wrapper { display: flex; align-items: center; flex-grow: 1; position: relative; }
.config-group input { flex-grow: 1; font-size: 11px; }
.config-group input[type=text] { padding-right: 30px; }
.config-pick-btn, .config-toggle-vis-btn { padding: 6px 8px; font-size: 10px; flex-shrink: 0; background-color: #4A5568; color: white; border: none; border-radius: 4px; cursor: pointer; }
.config-toggle-vis-btn { position: absolute; right: 1px; top: 1px; bottom: 1px; background-color: var(--bg-secondary); padding: 0 8px; }
.config-toggle-vis-btn svg { width: 16px; height: 16px; pointer-events: none; }
.config-group input[type=checkbox] { width: 16px; height: 16px; flex-grow: 0; }
#element-picker-overlay { position: fixed; background-color: rgba(0, 179, 115, 0.3); border: 2px solid var(--accent-green); z-index: 10000; pointer-events: none; display: none; transition: all 0.1s linear; }
#element-picker-tooltip { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: black; color: white; padding: 10px 20px; border-radius: 5px; z-index: 10001; display: none; }
.bot-modal-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 10002; display: flex; align-items: center; justify-content: center; }
.bot-modal-content { background: var(--bg-primary); padding: 20px; border-radius: 8px; border: 1px solid var(--bg-secondary); width: 90%; max-width: 500px; max-height: 90vh; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; }
#strategy-modal-content > .bot-input-group, #strategy-modal-content > .bot-btn, #strategy-modal-content > hr { margin: 0; }
#password-prompt-content { max-width: 380px; text-align: center; }
#strategy-rules-container { overflow-y: auto; flex-grow: 1; min-height: 100px; }
.strategy-rule { background: var(--bg-secondary); padding: 8px; border-radius: 4px; margin-bottom: 8px; }
.strategy-rule-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
.strategy-rule-summary { display: flex; gap: 5px; align-items: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.strategy-rule-summary span { font-size: 13px; }
.strategy-rule-summary .summary-action { color: var(--accent-yellow); }
.strategy-rule-controls { display: flex; align-items: center; }
.strategy-rule-editor { display: none; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--bg-primary); }
.strategy-rule.expanded .strategy-rule-editor { display: block; }
.strategy-rule.expanded .strategy-rule-summary { display: none; }
.rule-toggle-group { display: flex; justify-content: center; flex-wrap: wrap; gap: 5px; margin-bottom: 8px; }
.rule-toggle-group label { background: var(--bg-primary); padding: 5px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; }
.rule-toggle-group input { display: none; }
.rule-toggle-group input:checked + label { background: var(--accent-green); color: white; }
.rule-sentence { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; margin-bottom: 8px; }
.rule-sentence span { font-size: 13px; }
.rule-sentence .bot-input { width: auto; flex-grow: 1; min-width: 80px; padding: 4px 6px; }
.action-sentence .action-type { width: 180px; flex-grow: 0; flex-shrink: 0; }
.rule-sentence .bot-input[type=number] { max-width: 100px; }
.remove-btn, .move-btn { color: #fff; cursor: pointer; font-weight: bold; background: none; border: none; font-size: 18px; padding: 0 5px; }
.remove-btn { color: var(--accent-red); }
.btn-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.btn-grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
.btn-grid-4 { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 8px; }
.btn-grid-2 .bot-btn, .btn-grid-3 .bot-btn, .btn-grid-4 .bot-btn { margin-top: 0; padding: 8px 4px; }
hr { border: none; border-top: 1px solid var(--bg-secondary); margin: 10px 0; }
.resizer { position: absolute; z-index: 10; }
.resizer-r { cursor: e-resize; height: 100%; width: 5px; right: 0; top: 0; }
.resizer-l { cursor: w-resize; height: 100%; width: 5px; left: 0; top: 0; }
.resizer-b { cursor: s-resize; height: 5px; width: 100%; right: 0; bottom: 0; }
.resizer-t { cursor: n-resize; height: 5px; width: 100%; left: 0; top: 0; }
.resizer-br { cursor: se-resize; height: 15px; width: 15px; right: 0; bottom: 0; }
.resizer-bl { cursor: sw-resize; height: 15px; width: 15px; left: 0; bottom: 0; }
.resizer-tr { cursor: ne-resize; height: 15px; width: 15px; right: 0; top: 0; }
.resizer-tl { cursor: nw-resize; height: 15px; width: 15px; left: 0; top: 0; }
.resizer-br::after { content: ''; position: absolute; bottom: 0; right: 0; width: 0; height: 0; border-style: solid; border-width: 0 0 12px 12px; border-color: transparent transparent rgba(177, 186, 211, 0.2) transparent; pointer-events: none; }
#bot-notification-container { position: fixed; top: 20px; right: 20px; z-index: 10005; display: flex; flex-direction: column; gap: 10px; }
.bot-notification { background-color: var(--bg-secondary); color: white; padding: 15px; border-radius: 8px; box-shadow: 0 3px 10px rgba(0,0,0,0.3); width: 300px; opacity: 0; transform: translateX(100%); animation: slideIn 0.5s forwards; }
.bot-notification.fade-out { animation: slideOut 0.5s forwards; }
.bot-notification-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.bot-notification-header span { font-weight: bold; }
.bot-notification-header .close-btn { background: none; border: none; color: #b1bad3; font-size: 20px; cursor: pointer; line-height: 1; }
@keyframes slideIn { from { opacity: 0; transform: translateX(100%); } to { opacity: 1; transform: translateX(0); } }
@keyframes slideOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(100%); } }
.flex-container { display: flex; align-items: center; gap: 0.5rem; }
.justify-between { justify-content: space-between; }
#keno-pattern-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 5px; background-color: var(--bg-secondary); padding: 10px; border-radius: 4px; touch-action: none; }
.keno-pattern-tile { background-color: var(--bg-tertiary); color: var(--text-secondary); aspect-ratio: 1 / 1; display: flex; align-items: center; justify-content: center; border-radius: 4px; font-size: 12px; cursor: pointer; user-select: none; }
.keno-pattern-tile.selected { background-color: var(--accent-blue); color: white; }
.keno-pattern-tile.dragging { opacity: 0.5; }
#pattern-animation-settings { background: var(--bg-secondary); padding: 8px; border-radius: 4px; margin-top: 10px; }
#pattern-animation-frames-list option:checked { background-color: var(--accent-blue); color: white; }
.compact-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
`;
// --- CORE LOGIC ---
function init() {
if (botState.isBotInitialized) return;
botState.isBotInitialized = true;
loadConfig();
loadStrategies();
loadUserPatterns();
document.body.insertAdjacentHTML('beforeend', botHtml);
GM_addStyle(botCss);
document.getElementById('keno-bot-window').style.display = 'flex';
applyUiScale();
loadSecurityConfig();
populateConfigTab(); // Needs to happen after loadConfig for auth token
populateStrategyDropdowns();
populatePatternDropdowns();
setupEventListeners();
makeDraggable(document.getElementById('keno-bot-window'), document.getElementById('bot-header'));
makeDraggable(document.getElementById('keno-bot-minimized-bar'), document.getElementById('keno-bot-minimized-bar'));
makeResizable(document.getElementById('keno-bot-window'));
log("Bot UI Initialized. Waiting for game interface...", "system");
waitForGameAndEnable();
updateProfitDisplay();
if (botState.storedPasswordHash) {
lockUI();
} else {
updateAdminStatus("No password set. Bot is unlocked.", "info");
}
if (CONFIG.injectControlsToPage) {
originalInjectControls();
}
}
function setupEventListeners() {
// Window Controls
document.getElementById('minimize-btn').addEventListener('click', minimizeBot);
document.getElementById('close-btn').addEventListener('click', closeBot);
document.getElementById('maximize-btn').addEventListener('click', maximizeBot);
document.getElementById('close-minimized-btn').addEventListener('click', closeBot);
// Tab switching
setupTabSwitching();
// Manual Tab
document.getElementById('manual-btn-bar').addEventListener('click', () => handleManualPatternChange('type', 'bar'));
document.getElementById('manual-btn-bird').addEventListener('click', () => handleManualPatternChange('type', 'bird'));
document.getElementById('manual-btn-random').addEventListener('click', () => handleManualPatternChange('type', 'random'));
document.getElementById('manual-btn-custom').addEventListener('click', () => handleManualPatternChange('type', 'custom'));
document.getElementById('keno-custom-count-input').addEventListener('change', (e) => {
kenoState.customCount = parseInt(e.target.value, 10) || 3;
if (kenoState.type === 'custom') handleManualPatternChange('type', 'custom'); // Re-apply if active
});
document.getElementById('manual-btn-flip').addEventListener('click', () => handleManualPatternChange('flip'));
document.getElementById('manual-btn-left').addEventListener('click', () => handleManualPatternChange('position', 'left'));
document.getElementById('manual-btn-middle').addEventListener('click', () => handleManualPatternChange('position', 'middle'));
document.getElementById('manual-btn-right').addEventListener('click', () => handleManualPatternChange('position', 'right'));
document.getElementById('manual-risk-select').addEventListener('change', handleManualRiskChange);
document.getElementById('manual-change-seed-btn').addEventListener('click', handleManualSeedChange);
document.getElementById('manual-bet-btn').addEventListener('click', handleManualBet); // Added
// Auto Tab
document.getElementById('start-stop-btn').addEventListener('click', toggleBot);
document.getElementById('keno-games-input').addEventListener('change', (e) => {
CONFIG.autoGames = parseInt(e.target.value, 10) || 0;
// No need to save config immediately, just update the state
});
// Set initial value from CONFIG
document.getElementById('keno-games-input').value = CONFIG.autoGames;
// Patterns Tab
document.getElementById('create-pattern-btn').addEventListener('click', () => openPatternCreator());
document.getElementById('edit-pattern-btn').addEventListener('click', editSelectedPattern);
document.getElementById('delete-pattern-btn').addEventListener('click', deleteSelectedPattern);
document.getElementById('user-pattern-list').addEventListener('dblclick', editSelectedPattern);
// Pattern Modal
document.getElementById('save-pattern-btn').addEventListener('click', savePattern);
document.getElementById('cancel-pattern-btn').addEventListener('click', () => document.getElementById('pattern-creator-modal').style.display = 'none');
// Pattern Modal - Animation
document.getElementById('pattern-animation-enable').addEventListener('change', (e) => {
document.getElementById('pattern-animation-controls').style.display = e.target.checked ? 'block' : 'none';
});
document.getElementById('pattern-animation-type').addEventListener('change', (e) => {
document.getElementById('pattern-animation-frames-ui').style.display = e.target.value === 'custom' ? 'block' : 'none';
// Enable/disable grid dragging based on selection
patternCreatorState.isDragging = false;
});
document.getElementById('pattern-frame-add').addEventListener('click', addPatternFrame);
document.getElementById('pattern-frame-delete').addEventListener('click', deletePatternFrame);
document.getElementById('pattern-frame-up').addEventListener('click', () => movePatternFrame(-1));
document.getElementById('pattern-frame-down').addEventListener('click', () => movePatternFrame(1));
// Pattern Modal - Grid Listeners
const patternGrid = document.getElementById('keno-pattern-grid');
patternGrid.addEventListener('click', handlePatternGridClick);
patternGrid.addEventListener('mousedown', handlePatternGridDragStart);
patternGrid.addEventListener('mousemove', handlePatternGridDragMove);
patternGrid.addEventListener('mouseup', handlePatternGridDragEnd);
patternGrid.addEventListener('mouseleave', handlePatternGridDragEnd); // Stop drag if mouse leaves grid
// Advanced Strategy tab
document.getElementById('create-strategy-btn').addEventListener('click', () => openStrategyModal());
document.getElementById('edit-strategy-btn').addEventListener('click', editSelectedStrategy);
document.getElementById('delete-strategy-btn').addEventListener('click', deleteSelectedStrategy);
document.getElementById('import-strategy-btn').addEventListener('click', importStrategy);
document.getElementById('export-strategy-btn').addEventListener('click', exportSelectedStrategy);
document.getElementById('advanced-strategy-list').addEventListener('dblclick', editSelectedStrategy);
// Strategy Modal
document.getElementById('save-strategy-btn').addEventListener('click', saveStrategy);
document.getElementById('cancel-strategy-btn').addEventListener('click', () => document.getElementById('strategy-modal').style.display = 'none');
document.getElementById('add-rule-btn').addEventListener('click', () => addRuleToModal());
// Config tab
document.getElementById('save-config-btn').addEventListener('click', saveConfig);
document.getElementById('reset-config-btn').addEventListener('click', resetConfigToDefault);
// Log Tab
document.getElementById('save-log-btn').addEventListener('click', saveLogToFile);
document.getElementById('reset-stats-btn').addEventListener('click', resetStats);
document.getElementById('log-filters').addEventListener('change', (e) => {
if (e.target.matches('input[type="checkbox"]')) {
logFilters[e.target.dataset.filter] = e.target.checked;
applyLogFilters();
}
});
// Admin tab
document.getElementById('save-credentials-btn').addEventListener('click', saveCredentials);
document.getElementById('lock-unlock-btn').addEventListener('click', handleLockUnlock);
// Password Prompt
document.getElementById('password-prompt-submit').addEventListener('click', handleSubmitPassword);
document.getElementById('password-prompt-cancel').addEventListener('click', () => {
document.getElementById('password-prompt-modal').style.display = 'none';
botState.pendingAction = null;
});
document.getElementById('prompt-password-input').addEventListener('keydown', (e) => {
if (e.key === "Enter") {
e.preventDefault();
document.getElementById('password-prompt-submit').click();
}
});
// Keydown listener
document.addEventListener('keydown', handleKeydown);
}
function setupTabSwitching() {
const tabButtons = document.querySelectorAll('.bot-tab-btn');
const tabContents = document.querySelectorAll('.bot-tab-content');
tabButtons.forEach(btn => {
btn.addEventListener('click', (e) => {
if (botState.isLocked && e.currentTarget.dataset.tab !== 'manual' && e.currentTarget.dataset.tab !== 'admin') {
promptForPassword(unlockUI, "Enter password to unlock bot settings.");
return;
}
const tabName = e.currentTarget.dataset.tab;
tabButtons.forEach(b => b.classList.remove('active'));
e.currentTarget.classList.add('active');
tabContents.forEach(content => {
content.classList.toggle('active', content.id === `tab-${tabName}`);
});
});
});
}
// --- KENO CORE ACTIONS (Adapted from original script) ---
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function clearTable() {
const clearButton = document.querySelector(CONFIG.clearButton);
if (clearButton && !clearButton.hasAttribute('disabled')) {
// Use simulateClick for potentially better compatibility
simulateClick(clearButton);
await delay(100);
}
}
async function pickTilesByNumbers(numbersToPick) {
if (!numbersToPick || numbersToPick.length === 0) {
console.warn("pickTilesByNumbers called with no numbers.");
return false; // Indicate failure
}
const sortedNew = [...numbersToPick].sort((a,b) => a-b).toString();
const sortedLast = [...botState.lastPickedNumbers].sort((a,b) => a-b).toString();
if (sortedNew === sortedLast) {
// console.log("Numbers already selected, skipping clear/pick.");
return true; // Indicate success (already done)
}
await clearTable();
// Use the potentially broader selector from CONFIG
const tileElements = document.querySelectorAll(CONFIG.tile);
if (tileElements.length === 0) {
log("Keno tiles not found using selector: " + CONFIG.tile + ". Aborting pick.", "error");
botState.lastPickedNumbers = []; // Reset state if tiles disappear
return false; // Indicate failure
}
let pickedCount = 0;
for (const number of numbersToPick) {
// Find the specific tile button
const tileToClick = Array.from(tileElements).find(el => {
// Prioritize data-testid="tile-N"
const testId = el.getAttribute('data-testid');
if (testId === `tile-${number}`) {
return true;
}
// Fallback to checking span content
const span = el.querySelector('span.tile-number');
return span && parseInt(span.textContent, 10) === number;
});
if (tileToClick) {
// Use simulateClick for potentially better compatibility
simulateClick(tileToClick);
pickedCount++;
await delay(25); // Keep small delay
} else {
log(`Tile number ${number} not found.`, "error");
}
}
if (pickedCount === numbersToPick.length) {
botState.lastPickedNumbers = numbersToPick.slice();
// log(`Selected tiles: ${numbersToPick.join(', ')}`, "manual"); // Maybe too verbose for auto
return true; // Indicate success
} else {
log(`Failed to select all tiles. Expected ${numbersToPick.length}, picked ${pickedCount}.`, "error");
botState.lastPickedNumbers = []; // Reset if failed
return false; // Indicate failure
}
}
async function pickRandomTiles(count = 10) {
const tileElements = document.querySelectorAll(CONFIG.tile);
if (tileElements.length === 0) {
log("Cannot pick random tiles: Keno tile elements not found using selector: " + CONFIG.tile, "error");
botState.lastPickedNumbers = [];
await clearTable();
return false;
}
// Extract numbers, prioritizing data-testid, falling back to span
const availableTiles = Array.from(tileElements).map(el => {
const testId = el.getAttribute('data-testid');
if (testId && testId.startsWith('tile-')) {
return parseInt(testId.substring(5), 10);
}
// Fallback to span content
const span = el.querySelector('span.tile-number');
return span ? parseInt(span.textContent, 10) : null;
}).filter(num => num !== null && !isNaN(num)); // Filter out nulls and NaN
if (availableTiles.length === 0) {
log("Could not extract any tile numbers.", "error");
return false;
}
const pickedNumbers = [];
const numToPick = Math.min(count, 10, availableTiles.length); // Ensure we don't pick more than 10 or available
while (pickedNumbers.length < numToPick && availableTiles.length > 0) {
const randomIndex = Math.floor(Math.random() * availableTiles.length);
pickedNumbers.push(availableTiles.splice(randomIndex, 1)[0]);
}
return await pickTilesByNumbers(pickedNumbers);
}
async function applyCurrentPattern() {
let success = false;
// Handle animation logic first
if (kenoState.type === 'user' && kenoState.animation.enabled && kenoState.animation.patternName === kenoState.userPatternName) {
return await executePatternAnimation();
}
// --- Standard (non-animated) pattern logic ---
if (kenoState.type === 'custom') {
success = await pickRandomTiles(kenoState.customCount);
} else if (kenoState.type === 'random') {
success = await pickAndApplyRandomPattern();
} else if (kenoState.type === 'user') {
const pattern = userPatterns[kenoState.userPatternName];
if (pattern) {
// Apply the base frame (index 0) of the pattern
success = await pickTilesByNumbers(pattern.tiles);
} else {
log(`User pattern "${kenoState.userPatternName}" not found. Picking random.`, "error");
success = await pickAndApplyRandomPattern();
}
} else if (['bar', 'bird'].includes(kenoState.type) && kenoState.position) {
const patternKey = kenoState.flip ? `${kenoState.type}_flip` : kenoState.type;
const selectedPattern = patterns[patternKey]?.[kenoState.position];
if (selectedPattern) {
success = await pickTilesByNumbers(selectedPattern);
} else {
log(`Pattern not defined: ${patternKey} - ${kenoState.position}`, "error");
success = false;
}
} else {
// No valid pattern selected, check if tiles are already picked
if (botState.lastPickedNumbers.length > 0) {
log("No pattern selected, using previously picked tiles.", "manual");
success = true; // Tiles are already set
} else {
log("No valid pattern/position selected. Clearing table.", "manual");
await clearTable();
botState.lastPickedNumbers = [];
success = true; // Clearing is considered a success in this context
}
}
return success;
}
async function pickAndApplyRandomPattern() {
const allPatternList = Object.values(patterns).flatMap(type => Object.values(type));
const userPatternList = Object.values(userPatterns).map(p => p.tiles); // Use base tiles
const combinedList = [...allPatternList, ...userPatternList];
if (Math.random() > 0.5 && combinedList.length > 0) {
// Pick a preset or user pattern
const randomPattern = combinedList[Math.floor(Math.random() * combinedList.length)];
return await pickTilesByNumbers(randomPattern);
} else {
// Pick random tiles (between 1 and 10)
const randomCount = Math.floor(Math.random() * 10) + 1;
return await pickRandomTiles(randomCount);
}
}
// --- BOT LOGIC (Adapted from Dice Bot) ---
function toggleBot() {
if (botState.running) { // Stopping
botState.running = false;
if (botState.autoBetLoopInterval) {
clearTimeout(botState.autoBetLoopInterval);
botState.autoBetLoopInterval = null;
}
const btn = document.getElementById('start-stop-btn');
btn.textContent = 'Start Auto (S)';
btn.classList.remove('bot-btn-danger');
btn.classList.add('bot-btn-primary');
updateStatus('Stopped');
log("Auto betting stopped.", "auto");
updateInputDisabledState();
} else { // Starting
// Check if any tiles are selected if using manual selection without strategy
const autoStrategyName = document.getElementById('auto-strategy-select').value;
if (autoStrategyName === 'none' && botState.lastPickedNumbers.length === 0) {
// Try to apply pattern first
applyCurrentPattern().then(patternSet => {
if (!patternSet || botState.lastPickedNumbers.length === 0) {
showNotification("Please select a pattern or tiles manually before starting Auto mode without a strategy.", "error");
} else {
startBotInternal(); // Start if pattern was applied
}
});
} else {
startBotInternal(); // Start if strategy is selected or tiles already picked
}
}
}
function startBotInternal() {
botState.running = true;
const btn = document.getElementById('start-stop-btn');
btn.textContent = 'Stop Auto (S)';
btn.classList.remove('bot-btn-primary');
btn.classList.add('bot-btn-danger');
updateStatus('Auto Running...');
updateInputDisabledState();
startBot();
}
async function startBot() {
const numGamesInput = document.getElementById('keno-games-input');
const numGames = parseInt(numGamesInput.value, 10);
const gameLimitText = (numGames <= 0) ? 'infinite games' : `${numGames} games`;
log(`Starting auto-bet session for ${gameLimitText}.`, "auto");
resetStats(); // Reset stats each time auto starts
// Ensure initial risk level is set
await setRiskLevel(kenoState.riskLevel);
// Ensure initial pattern is set if not using a strategy
const autoStrategyName = document.getElementById('auto-strategy-select').value;
if (autoStrategyName === 'none') {
const patternSet = await applyCurrentPattern();
if (!patternSet) {
log("Failed to set initial pattern. Stopping bot.", "error");
toggleBot(); // Stop if pattern couldn't be set
return;
}
}
botState.autoBetLoopInterval = setTimeout(autoBetLoop, 100);
}
async function autoBetLoop() {
if (!botState.running) return;
await waitForGameToFinish();
if (!botState.running) return;
const autoStrategyName = document.getElementById('auto-strategy-select').value;
const useAdvancedStrategy = autoStrategyName !== 'none' && advancedStrategies[autoStrategyName];
let patternSetSuccessfully = true;
if (useAdvancedStrategy) {
// Strategy execution will handle tile picking
patternSetSuccessfully = await executeAdvancedStrategy(advancedStrategies[autoStrategyName]);
} else {
// Apply manual selection (which includes animation logic)
// This will either apply a static pattern or the next animation frame
patternSetSuccessfully = await applyCurrentPattern();
}
// Stop if pattern failed (e.g., elements not found)
if (!patternSetSuccessfully) {
log("Failed to set tiles. Stopping bot.", "error");
if (botState.running) toggleBot();
return;
}
// Add a small delay after picking tiles before betting
await delay(150);
if (!botState.running) return; // Check again after delay
const betAmount = getStakeBetAmount();
if (betAmount <= 0) {
log("Bet amount is zero or invalid. Stopping bot.", "error");
if (botState.running) toggleBot();
return;
}
const result = await placeBetAndGetResult(betAmount);
if (result === null) {
log("Failed to place bet or get result. Stopping bot.", "error");
if (botState.running) toggleBot();
return;
}
// Log auto bet result
log(`Bet #${botState.betCount + 1}: ${result.win ? `WIN (x${result.multiplier.toFixed(2)})` : 'LOSS (x0.00)'}. Profit: ${result.profit.toFixed(8)}`, "auto");
updateStateFromResult(result);
// Game count check
const numGamesInput = document.getElementById('keno-games-input');
let gamesRemaining = parseInt(numGamesInput.value, 10);
if (!isNaN(gamesRemaining) && gamesRemaining > 0) {
gamesRemaining--;
numGamesInput.value = gamesRemaining;
if (gamesRemaining <= 0) {
log("Finished requested number of games.", "auto");
toggleBot();
return; // Stop the loop
}
}
// Continue loop if still running
if (botState.running) {
// Use a slightly longer delay between bets for Keno
botState.autoBetLoopInterval = setTimeout(autoBetLoop, 1500); // Increased delay
}
}
async function waitForGameToFinish() {
let waitAttempts = 60; // 30 seconds timeout
while (waitAttempts > 0) {
const betButton = document.querySelector(CONFIG.betButton);
// Check if the button exists and is enabled
if (betButton && !betButton.disabled && !betButton.hasAttribute('disabled')) {
await delay(300); // Shorter settle delay might be ok
return;
}
await delay(500);
waitAttempts--;
}
log("Timed out waiting for bet button to become active. Stopping auto-bet.", "error");
if (botState.running) toggleBot();
// Throw an error or return a specific value to signal timeout in autoBetLoop if needed
}
function placeBetAndGetResult(betAmount) {
return new Promise(resolve => {
const betButton = document.querySelector(CONFIG.betButton);
if (!betButton || betButton.disabled || betButton.hasAttribute('disabled')) {
updateStatus("ERROR: Bet button not active!");
resolve(null);
return;
}
const pastBetsContainer = document.querySelector(CONFIG.pastBetsContainer);
if (!pastBetsContainer) {
updateStatus("ERROR: Past bets list not found!");
resolve(null);
return;
}
const lastBetElement = pastBetsContainer.firstElementChild;
const lastBetId = lastBetElement?.getAttribute('data-past-bet-id') || null;
simulateClick(betButton); // Use simulateClick
let attempts = 0;
const maxAttempts = 75; // ~15 seconds timeout
const pollInterval = setInterval(() => {
const newBetElement = pastBetsContainer.firstElementChild;
const newBetId = newBetElement?.getAttribute('data-past-bet-id') || null;
// Check if a new bet has appeared in the history
if (newBetElement && newBetId && newBetId !== lastBetId) {
clearInterval(pollInterval);
const textContent = newBetElement.textContent || "";
// Regex to find multiplier like "x12.34" or "x1,234.5"
const multiplierMatch = textContent.match(/x([\d,.]+)/);
let multiplier = 0;
if (multiplierMatch && multiplierMatch[1]) {
// Remove commas before parsing
multiplier = parseFloat(multiplierMatch[1].replace(/,/g, ''));
}
const isWin = multiplier > 0;
// Calculate profit based on multiplier and bet amount
const profit = isWin ? (betAmount * multiplier) - betAmount : -betAmount;
// Log more detailed info
// log(`Bet #${botState.betCount + 1}: ${isWin ? `WIN (x${multiplier.toFixed(2)})` : 'LOSS (x0.00)'}. Profit: ${profit.toFixed(8)}`, botState.running ? "auto" : "manual");
resolve({
win: isWin,
profit: profit,
multiplier: multiplier
});
return;
}
if (++attempts >= maxAttempts) {
clearInterval(pollInterval);
updateStatus("ERROR: Bet result timed out!");
log("Bet result timed out.", "error");
resolve(null);
}
}, 200);
});
}
function updateStateFromResult(result) {
botState.betCount++;
botState.totalProfit += result.profit;
botState.lastBetWasWin = result.win;
botState.lastMultiplier = result.multiplier;
if (result.win) {
botState.winStreak++;
botState.lossStreak = 0;
} else {
botState.lossStreak++;
botState.winStreak = 0;
}
updateProfitDisplay();
}
function resetStats() {
botState.betCount = 0;
botState.totalProfit = 0;
botState.winStreak = 0;
botState.lossStreak = 0;
botState.lastBetWasWin = null;
botState.lastMultiplier = 0;
botState.strategyFlags = {};
botState.persistentTriggerStates = {};
// Reset animation state
kenoState.animation.currentFrame = 0;
kenoState.animation.currentAnchor = { row: 0, col: 0 };
kenoState.animation.currentVelocity = { row: 1, col: 1 };
updateProfitDisplay();
log("Stats reset.", "system");
}
// --- MANUAL CONTROLS ---
function handleManualPatternChange(category, value) {
let changed = false;
if (category === 'type') {
if (kenoState.type !== value) {
kenoState.type = value;
kenoState.userPatternName = null; // Clear user pattern if type changes
kenoState.animation.enabled = false; // Disable animation
changed = true;
} else { // Click again to deselect
kenoState.type = null;
kenoState.animation.enabled = false;
changed = true;
}
} else if (category === 'position') {
if (!['bar', 'bird'].includes(kenoState.type)) return; // Only allow for relevant types
if (kenoState.position !== value) {
kenoState.position = value;
changed = true;
} else { // Click again to deselect
kenoState.position = null;
changed = true;
}
} else if (category === 'flip') {
if (!['bar', 'bird'].includes(kenoState.type)) return; // Only allow for relevant types
kenoState.flip = !kenoState.flip;
changed = true;
} else if (category === 'user') {
if (kenoState.type !== 'user' || kenoState.userPatternName !== value) {
kenoState.type = 'user';
kenoState.userPatternName = value;
// Deselect bar/bird specific settings
kenoState.position = null;
kenoState.flip = false;
changed = true;
// Load animation state for this pattern
const pattern = userPatterns[value];
if (pattern && pattern.animation && pattern.animation.enabled) {
kenoState.animation.enabled = true;
kenoState.animation.type = pattern.animation.type;
kenoState.animation.patternName = value;
kenoState.animation.metadata = pattern.metadata;
kenoState.animation.customFrames = pattern.animation.customFrames || [];
kenoState.animation.currentFrame = 0;
kenoState.animation.currentAnchor = { row: 0, col: 0 }; // Reset anchor
kenoState.animation.currentVelocity = { row: 1, col: 1 }; // Reset velocity
} else {
kenoState.animation.enabled = false;
}
} else { // Click again to deselect
kenoState.type = null;
kenoState.userPatternName = null;
kenoState.animation.enabled = false;
changed = true;
}
}
updateManualButtonStates();
if (changed) {
// Apply the base pattern, don't trigger animation step here
// applyCurrentPattern();
// Let's apply the base frame of the pattern, but not step animation
const pattern = userPatterns[kenoState.userPatternName];
if (kenoState.type === 'user' && pattern) {
pickTilesByNumbers(pattern.tiles); // Pick base frame
} else {
applyCurrentPattern(); // Apply logic for bar/bird/etc.
}
}
}
async function handleManualRiskChange(event) {
const newRisk = event.target.value;
await setRiskLevel(newRisk);
}
async function setRiskLevel(level) {
const success = setUIValue(CONFIG.riskSelect, level);
if (success) {
kenoState.riskLevel = level;
log(`Risk level set to: ${level}`, "manual");
// Update UI dropdown if change came from strategy
const manualSelect = document.getElementById('manual-risk-select');
if (manualSelect && manualSelect.value !== level) {
manualSelect.value = level;
}
} else {
log(`Failed to set risk level to ${level}. Selector: ${CONFIG.riskSelect}`, "error");
}
return success;
}
function updateManualButtonStates() {
// Clear all active states first
document.querySelectorAll('#tab-manual .bot-btn.active').forEach(b => b.classList.remove('active'));
document.querySelectorAll('#manual-user-patterns-container .bot-btn.active').forEach(b => b.classList.remove('active'));
// Update Type buttons
const typeButtons = ['bar', 'bird', 'random', 'custom'];
typeButtons.forEach(type => {
const btn = document.getElementById(`manual-btn-${type}`);
if (btn) btn.classList.toggle('active', kenoState.type === type);
});
// Update User Pattern button
if (kenoState.type === 'user' && kenoState.userPatternName) {
const userBtn = document.querySelector(`#manual-user-patterns-container button[data-value="${kenoState.userPatternName}"]`);
if (userBtn) userBtn.classList.add('active');
}
// Update Position buttons
const positionButtons = ['left', 'middle', 'right'];
const posFlipDisabled = !['bar', 'bird'].includes(kenoState.type);
positionButtons.forEach(pos => {
const btn = document.getElementById(`manual-btn-${pos}`);
if (btn) {
btn.disabled = posFlipDisabled;
btn.classList.toggle('active', kenoState.position === pos && !posFlipDisabled);
}
});
// Update Flip button
const flipBtn = document.getElementById('manual-btn-flip');
if (flipBtn) {
flipBtn.disabled = posFlipDisabled;
flipBtn.classList.toggle('active', kenoState.flip && !posFlipDisabled);
}
// Update Risk dropdown
const riskSelect = document.getElementById('manual-risk-select');
if (riskSelect) riskSelect.value = kenoState.riskLevel;
// Reset irrelevant state if type disabled them
if (posFlipDisabled) {
// Keep state if type is 'user', otherwise reset
if (kenoState.type !== 'user') {
kenoState.position = null;
kenoState.flip = false;
}
}
// Update animation status text
const animStatusEl = document.getElementById('manual-pattern-animation-status');
if (animStatusEl) {
if (kenoState.animation.enabled) {
const typeText = document.querySelector(`#pattern-animation-type option[value="${kenoState.animation.type}"]`)?.textContent || 'Active';
animStatusEl.textContent = `Animation: ${typeText}`;
animStatusEl.style.display = '';
} else {
animStatusEl.style.display = 'none';
}
}
}
async function handleManualBet() {
if (botState.running) {
showNotification("Please stop Auto-bet before placing a manual bet.", "error");
return;
}
const betButton = document.querySelector(CONFIG.betButton);
if (betButton && (betButton.disabled || betButton.hasAttribute('disabled'))) {
showNotification("Please wait for the previous bet to finish.", "info");
return;
}
const manualBetBtn = document.getElementById('manual-bet-btn');
manualBetBtn.disabled = true;
manualBetBtn.textContent = "Betting...";
updateStatus("Manual Bet...");
try {
const manualStrategyName = document.getElementById('manual-strategy-select').value;
const useAdvancedStrategy = manualStrategyName !== 'none' && advancedStrategies[manualStrategyName];
let patternSetSuccessfully = true;
if (useAdvancedStrategy) {
// Manually log this, as executeAdvancedStrategy is usually quiet
log(`<b>Manual Step:</b> Applying strategy '${manualStrategyName}'...`, "manual");
// Note: We are not in an auto-bet loop, so state (streaks) persists
patternSetSuccessfully = await executeAdvancedStrategy(advancedStrategies[manualStrategyName]);
} else {
log("<b>Manual Step:</b> Applying manual selection...", "manual");
// applyCurrentPattern will trigger animation step if enabled
patternSetSuccessfully = await applyCurrentPattern();
}
if (!patternSetSuccessfully) {
throw new Error("Failed to set tiles. Check pattern/strategy.");
}
await delay(150); // Small delay after picking
const betAmount = getStakeBetAmount();
if (betAmount <= 0) {
throw new Error("Bet amount is zero or invalid.");
}
const result = await placeBetAndGetResult(betAmount);
if (result === null) {
throw new Error("Failed to place bet or get result.");
}
// Log manual bet result
log(`<b>Manual Bet #${botState.betCount + 1}:</b> ${result.win ? `WIN (x${result.multiplier.toFixed(2)})` : 'LOSS (x0.00)'}. Profit: ${result.profit.toFixed(8)}`, "manual");
updateStateFromResult(result);
updateStatus("Ready");
} catch (error) {
log(`Manual Bet Failed: ${error.message}`, "error");
showNotification(`Manual Bet Failed: ${error.message}`, "error");
updateStatus("Error");
} finally {
manualBetBtn.disabled = false;
manualBetBtn.textContent = "Manual Bet (B)";
}
}
// --- PATTERN CREATOR ---
// ... (loadUserPatterns, saveUserPatterns, populatePatternDropdowns remain the same) ...
function loadUserPatterns() {
const saved = localStorage.getItem('stakeKenoUserPatterns');
if (saved) {
try {
userPatterns = JSON.parse(saved);
// Ensure legacy patterns have new structure
for(const name in userPatterns) {
if (Array.isArray(userPatterns[name])) { // Very old format
userPatterns[name] = {
tiles: userPatterns[name],
metadata: calculatePatternMetadata(userPatterns[name]),
animation: { enabled: false, type: 'none', customFrames: [] }
}
} else if (!userPatterns[name].metadata) { // New format missing metadata
userPatterns[name].metadata = calculatePatternMetadata(userPatterns[name].tiles);
}
if (!userPatterns[name].animation) { // Missing animation block
userPatterns[name].animation = { enabled: false, type: 'none', customFrames: [] };
}
}
} catch (e) {
log("Failed to load user patterns.", "error");
userPatterns = {};
}
}
}
function saveUserPatterns() {
localStorage.setItem('stakeKenoUserPatterns', JSON.stringify(userPatterns));
populatePatternDropdowns();
populateStrategyDropdowns(); // Update strategy actions
}
function populatePatternDropdowns() {
const list = document.getElementById('user-pattern-list');
const manualContainer = document.getElementById('manual-user-patterns-container');
const strategySelects = document.querySelectorAll('.strategy-action-pattern-select'); // Ensure this class exists in strategy modal
const selectedListValue = list.value; // Preserve selection
list.innerHTML = '';
manualContainer.innerHTML = '';
strategySelects.forEach(sel => { // Clear previous options
const firstOption = sel.options[0]; // Preserve placeholder if exists
sel.innerHTML = '';
if (firstOption) sel.add(firstOption);
});
const patternNames = Object.keys(userPatterns).sort();
if (patternNames.length === 0) {
list.innerHTML = '<option disabled>No patterns created yet.</option>';
manualContainer.innerHTML = '<p class="strategy-desc" style="margin: 0;">Create patterns in the "Patterns" tab.</p>';
} else {
patternNames.forEach(name => {
// Add to patterns list
list.add(new Option(name, name));
// Add to strategy dropdowns
strategySelects.forEach(sel => {
sel.add(new Option(name, name));
});
// Add to manual tab
const btn = document.createElement('button');
btn.className = 'bot-btn bot-btn-secondary';
btn.textContent = name;
btn.dataset.category = 'user';
btn.dataset.value = name;
btn.onclick = () => handleManualPatternChange('user', name);
manualContainer.appendChild(btn);
});
list.value = selectedListValue; // Restore selection
}
updateManualButtonStates(); // Update manual buttons after repopulating
}
function openPatternCreator(patternName = '') {
const modal = document.getElementById('pattern-creator-modal');
const nameInput = document.getElementById('pattern-name');
const grid = document.getElementById('keno-pattern-grid');
// Reset grid
grid.querySelectorAll('.keno-pattern-tile.selected').forEach(t => t.classList.remove('selected'));
// Reset animation UI
document.getElementById('pattern-animation-enable').checked = false;
document.getElementById('pattern-animation-controls').style.display = 'none';
document.getElementById('pattern-animation-type').value = 'none';
document.getElementById('pattern-animation-frames-ui').style.display = 'none';
document.getElementById('pattern-animation-frames-list').innerHTML = '';
if (patternName && userPatterns[patternName]) {
// Edit mode
const pattern = userPatterns[patternName];
document.getElementById('pattern-modal-title').textContent = 'Edit Pattern';
nameInput.value = patternName;
nameInput.dataset.originalName = patternName;
// Load base tiles
pattern.tiles.forEach(num => {
const tile = grid.querySelector(`.keno-pattern-tile[data-number="${num}"]`);
if (tile) tile.classList.add('selected');
});
// Load animation settings
if (pattern.animation) {
const animEnabled = pattern.animation.enabled;
document.getElementById('pattern-animation-enable').checked = animEnabled;
document.getElementById('pattern-animation-controls').style.display = animEnabled ? 'block' : 'none';
document.getElementById('pattern-animation-type').value = pattern.animation.type || 'none';
document.getElementById('pattern-animation-frames-ui').style.display = (animEnabled && pattern.animation.type === 'custom') ? 'block' : 'none';
// Load custom frames
const framesList = document.getElementById('pattern-animation-frames-list');
framesList.innerHTML = '';
if (pattern.animation.customFrames && pattern.animation.customFrames.length > 0) {
pattern.animation.customFrames.forEach((frameTiles, index) => {
const frameName = `Frame ${index + 1} (${frameTiles.length} tiles)`;
const opt = new Option(frameName, index);
opt.dataset.tiles = JSON.stringify(frameTiles);
framesList.add(opt);
});
}
}
} else {
// Create mode
document.getElementById('pattern-modal-title').textContent = 'Create New Pattern';
nameInput.value = '';
nameInput.dataset.originalName = '';
}
modal.style.display = 'flex';
}
function handlePatternGridClick(e) {
// Only allow clicking if not dragging or animation is not 'custom'
const animType = document.getElementById('pattern-animation-type').value;
const animEnabled = document.getElementById('pattern-animation-enable').checked;
if (patternCreatorState.isDragging || (animEnabled && animType === 'custom')) {
return;
}
if (e.target.classList.contains('keno-pattern-tile')) {
const tile = e.target;
const grid = document.getElementById('keno-pattern-grid');
const selectedCount = grid.querySelectorAll('.keno-pattern-tile.selected').length;
if (tile.classList.contains('selected')) {
tile.classList.remove('selected');
} else if (selectedCount < 10) {
tile.classList.add('selected');
} else {
showNotification("You can only select up to 10 tiles.", "error");
}
}
}
// --- Pattern Dragging Logic ---
function handlePatternGridDragStart(e) {
const animType = document.getElementById('pattern-animation-type').value;
const animEnabled = document.getElementById('pattern-animation-enable').checked;
if (!animEnabled || animType !== 'custom') return; // Only drag in custom anim mode
const tile = e.target.closest('.keno-pattern-tile');
if (tile && tile.classList.contains('selected')) {
e.preventDefault();
patternCreatorState.isDragging = true;
patternCreatorState.dragStartTile = parseInt(tile.dataset.number, 10);
patternCreatorState.dragStartCoords = getTileCoords(patternCreatorState.dragStartTile);
// Get current selected tiles and metadata
const currentTiles = Array.from(document.querySelectorAll('#keno-pattern-grid .keno-pattern-tile.selected'))
.map(t => parseInt(t.dataset.number, 10));
patternCreatorState.currentTiles = currentTiles;
patternCreatorState.currentMetadata = calculatePatternMetadata(currentTiles);
tile.classList.add('dragging');
}
}
function handlePatternGridDragMove(e) {
if (!patternCreatorState.isDragging) return;
const tile = e.target.closest('.keno-pattern-tile');
if (!tile) return; // Moved off grid or onto gap
e.preventDefault();
const hoverTile = parseInt(tile.dataset.number, 10);
const hoverCoords = getTileCoords(hoverTile);
const metadata = patternCreatorState.currentMetadata;
if (!metadata) return; // Should not happen
// Calculate delta from where the drag *started*
const deltaRow = hoverCoords.row - patternCreatorState.dragStartCoords.row;
const deltaCol = hoverCoords.col - patternCreatorState.dragStartCoords.col;
// Find the new anchor (top-left of bounding box)
let newAnchorRow = metadata.minRow + deltaRow;
let newAnchorCol = metadata.minCol + deltaCol;
// Constrain anchor to valid grid positions
newAnchorRow = Math.max(0, Math.min(newAnchorRow, metadata.validAnchorRows));
newAnchorCol = Math.max(0, Math.min(newAnchorCol, metadata.validAnchorCols));
// Shift pattern based on *offsets from bounding box*
const newTiles = shiftPattern(metadata.offsets, newAnchorRow, newAnchorCol);
// Update grid UI
document.querySelectorAll('#keno-pattern-grid .keno-pattern-tile').forEach(t => {
t.classList.toggle('selected', newTiles.includes(parseInt(t.dataset.number, 10)));
});
}
function handlePatternGridDragEnd(e) {
if (!patternCreatorState.isDragging) return;
e.preventDefault();
patternCreatorState.isDragging = false;
patternCreatorState.dragStartTile = null;
patternCreatorState.dragStartCoords = null;
patternCreatorState.currentMetadata = null; // Clear metadata cache
document.querySelectorAll('#keno-pattern-grid .keno-pattern-tile.dragging').forEach(t => t.classList.remove('dragging'));
}
// --- Pattern Frame Logic ---
function addPatternFrame() {
const list = document.getElementById('pattern-animation-frames-list');
const selectedTiles = Array.from(document.querySelectorAll('#keno-pattern-grid .keno-pattern-tile.selected'))
.map(t => parseInt(t.dataset.number, 10)).sort((a,b) => a-b);
if (selectedTiles.length === 0) {
showNotification("Select tiles on the grid to save a frame.", "error");
return;
}
const frameName = `Frame ${list.options.length + 1} (${selectedTiles.length} tiles)`;
const opt = new Option(frameName, list.options.length);
opt.dataset.tiles = JSON.stringify(selectedTiles);
list.add(opt);
list.selectedIndex = list.options.length - 1; // Select new frame
}
function deletePatternFrame() {
const list = document.getElementById('pattern-animation-frames-list');
const selectedIndex = list.selectedIndex;
if (selectedIndex === -1) {
showNotification("Select a frame to delete.", "error");
return;
}
list.remove(selectedIndex);
// Re-index subsequent frames
for (let i = selectedIndex; i < list.options.length; i++) {
list.options[i].text = `Frame ${i + 1} (${JSON.parse(list.options[i].dataset.tiles).length} tiles)`;
list.options[i].value = i;
}
}
function movePatternFrame(direction) {
const list = document.getElementById('pattern-animation-frames-list');
const index = list.selectedIndex;
if (index === -1) return;
const newIndex = index + direction;
if (newIndex < 0 || newIndex >= list.options.length) return; // Out of bounds
// Simple swap
const otherOpt = list.options[newIndex];
const currentOpt = list.options[index];
// Swap data
const tempText = currentOpt.text;
const tempTiles = currentOpt.dataset.tiles;
currentOpt.text = otherOpt.text;
currentOpt.dataset.tiles = otherOpt.dataset.tiles;
otherOpt.text = tempText;
otherOpt.dataset.tiles = tempTiles;
// Re-index text
list.options[index].text = `Frame ${index + 1} (${JSON.parse(list.options[index].dataset.tiles).length} tiles)`;
list.options[newIndex].text = `Frame ${newIndex + 1} (${JSON.parse(list.options[newIndex].dataset.tiles).length} tiles)`;
// Move selection
list.selectedIndex = newIndex;
}
function savePattern() {
const nameInput = document.getElementById('pattern-name');
const originalName = nameInput.dataset.originalName || '';
const newName = nameInput.value.trim();
if (!newName) {
showNotification("Pattern name cannot be empty.", "error");
return;
}
if (newName !== originalName && userPatterns[newName]) {
showNotification("A pattern with this name already exists.", "error");
return;
}
const selectedTiles = Array.from(document.querySelectorAll('#keno-pattern-grid .keno-pattern-tile.selected'))
.map(t => parseInt(t.dataset.number, 10));
if (selectedTiles.length === 0 || selectedTiles.length > 10) { // Added max check
showNotification("Please select between 1 and 10 tiles for the base pattern.", "error");
return;
}
// Calculate metadata for the base pattern
const metadata = calculatePatternMetadata(selectedTiles);
// Get animation settings
const animEnabled = document.getElementById('pattern-animation-enable').checked;
const animType = document.getElementById('pattern-animation-type').value;
const animFramesList = document.getElementById('pattern-animation-frames-list');
let customFrames = [];
if (animEnabled && animType === 'custom') {
customFrames = Array.from(animFramesList.options).map(opt => JSON.parse(opt.dataset.tiles));
if (customFrames.length === 0) {
showNotification("Custom animation is enabled, but no frames were saved. Saving pattern without animation.", "info");
}
}
const animationSettings = {
enabled: animEnabled,
type: animType,
customFrames: customFrames
};
// Delete old pattern if renaming
if (originalName && newName !== originalName) {
delete userPatterns[originalName];
// If the active pattern was this one, clear it
if (kenoState.userPatternName === originalName) {
kenoState.type = null;
kenoState.userPatternName = null;
kenoState.animation.enabled = false;
}
}
userPatterns[newName] = {
tiles: selectedTiles.sort((a,b)=>a-b), // Store sorted base frame
metadata: metadata,
animation: animationSettings
};
saveUserPatterns();
document.getElementById('pattern-creator-modal').style.display = 'none';
log(`Pattern "${newName}" saved.`, "system");
}
function editSelectedPattern() {
const selectedName = document.getElementById('user-pattern-list').value;
if (selectedName) {
openPatternCreator(selectedName);
} else {
showNotification("Please select a pattern to edit.", "error");
}
}
function deleteSelectedPattern() {
const selectedName = document.getElementById('user-pattern-list').value;
if (selectedName && confirm(`Are you sure you want to delete the pattern "${selectedName}"?`)) {
delete userPatterns[selectedName];
saveUserPatterns();
log(`Pattern "${selectedName}" deleted.`, "system");
}
}
// --- ADVANCED STRATEGY LOGIC ---
function openStrategyModal(strategyName = '') {
const modal = document.getElementById('strategy-modal');
const nameInput = document.getElementById('strategy-name');
const rulesContainer = document.getElementById('strategy-rules-container');
document.getElementById('strategy-modal-title').textContent = strategyName ? 'Edit Strategy' : 'Create Strategy';
nameInput.value = strategyName;
nameInput.dataset.originalName = strategyName;
rulesContainer.innerHTML = '';
if (strategyName && advancedStrategies[strategyName]) {
advancedStrategies[strategyName].forEach(rule => addRuleToModal(rule));
} else {
addRuleToModal(); // Add one empty rule by default
}
modal.style.display = 'flex';
}
function addRuleToModal(rule = {}) {
const rulesContainer = document.getElementById('strategy-rules-container');
const ruleDiv = document.createElement('div');
ruleDiv.className = 'strategy-rule';
const ruleId = `rule-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
ruleDiv.id = ruleId;
const defaults = {
conditionType: 'bets', // Default condition type
playConditionTerm: 'every',
playConditionValue: 1,
playConditionBetType: 'lose',
netGainCondition: 'greater',
netGainValue: 0,
multiplierCondition: 'gte',
multiplierValue: 10,
action: 'changeTilesRandom', // Default action
actionValue: '', // For preset/user patterns/notify text
actionValueRisk: 'medium' // Default risk level for setRisk action
};
const currentRule = { ...defaults, ...rule };
// Ensure numeric values are numbers, provide defaults if NaN
currentRule.playConditionValue = Number(currentRule.playConditionValue) || defaults.playConditionValue;
currentRule.netGainValue = Number(currentRule.netGainValue) || defaults.netGainValue;
currentRule.multiplierValue = Number(currentRule.multiplierValue) || defaults.multiplierValue;
ruleDiv.innerHTML = `
<div class="strategy-rule-header">
<div class="strategy-rule-summary"></div>
<div class="strategy-rule-controls">
<button class="move-btn" data-direction="up" title="Move Up">↑</button>
<button class="move-btn" data-direction="down" title="Move Down">↓</button>
<button class="remove-btn" title="Remove Rule">×</button>
</div>
</div>
<div class="strategy-rule-editor">
<!-- Condition -->
<div class="rule-toggle-group">
<input type="radio" id="${ruleId}-type-bets" name="${ruleId}-type" value="bets" ${currentRule.conditionType === 'bets' ? 'checked' : ''}>
<label for="${ruleId}-type-bets">Play</label>
<input type="radio" id="${ruleId}-type-profit" name="${ruleId}-type" value="profit" ${currentRule.conditionType === 'profit' ? 'checked' : ''}>
<label for="${ruleId}-type-profit">Net Gain</label>
<input type="radio" id="${ruleId}-type-multiplier" name="${ruleId}-type" value="multiplier" ${currentRule.conditionType === 'multiplier' ? 'checked' : ''}>
<label for="${ruleId}-type-multiplier">Multiplier</label>
</div>
<div class="play-condition-sentence rule-sentence" style="display: ${currentRule.conditionType === 'bets' ? 'flex': 'none'}">
<span>On</span>
<select class="bot-input play-term">
<option value="every">Every</option>
<option value="everyStreakOf">Every streak of</option>
<option value="firstStreakOf">First streak of</option>
<option value="streakGreaterThan">Streak greater than</option>
</select>
<input type="number" class="bot-input play-value" value="${currentRule.playConditionValue}">
<select class="bot-input play-bet-type">
<option value="win">Wins</option>
<option value="lose">Losses</option>
<option value="bet">Games</option>
</select>
</div>
<div class="net-gain-condition-sentence rule-sentence" style="display: ${currentRule.conditionType === 'profit' ? 'flex': 'none'}">
<span>If Net Gain is</span>
<select class="bot-input net-gain-condition">
<option value="greater">></option>
<option value="less"><</option>
<option value="=">=</option>
</select>
<input type="number" step="any" class="bot-input net-gain-value" value="${currentRule.netGainValue}">
</div>
<div class="multiplier-condition-sentence rule-sentence" style="display: ${currentRule.conditionType === 'multiplier' ? 'flex': 'none'}">
<span>If Multiplier is</span>
<select class="bot-input multiplier-condition">
<option value="gte">≥</option>
<option value="lte">≤</option>
<option value="eq">=</option>
</select>
<input type="number" step="any" class="bot-input multiplier-value" value="${currentRule.multiplierValue}">
<span>x</span>
</div>
<!-- Action -->
<div class="action-sentence rule-sentence">
<span>Do</span>
<select class="bot-input action-type">
<option value="changeTilesRandom">Change Tiles (Random)</option>
<option value="changeTilesPreset">Change Tiles (Preset)</option>
<option value="changeTilesUser">Change Tiles (User Pattern)</option>
<option value="setRiskLevel">Set Risk Level</option> <!-- Added Risk -->
<option value="changeSeed">Change Seed (API)</option> <!-- Added Seed -->
<option value="stop">Stop Autoplay</option>
<option value="notify">Show Notification</option>
</select>
<select class="bot-input action-value-preset" style="display: none;"></select>
<select class="bot-input action-value-user strategy-action-pattern-select" style="display: none;"></select>
<select class="bot-input action-value-risk" style="display: none;">
<option value="classic">Classic</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<input type="text" class="bot-input action-value-text" style="display: none;" placeholder="Notification message...">
</div>
</div>
`;
rulesContainer.appendChild(ruleDiv);
// Populate dynamic dropdowns
const presetSelect = ruleDiv.querySelector('.action-value-preset');
presetSelect.innerHTML = ''; // Clear existing
for (const pKey in patterns) {
for (const posKey in patterns[pKey]) {
const name = `${pKey.replace(/_/g, ' ')} - ${posKey}`;
const value = `${pKey}:${posKey}`;
presetSelect.add(new Option(name, value));
}
}
const userSelect = ruleDiv.querySelector('.action-value-user');
userSelect.innerHTML = ''; // Clear existing
for (const name in userPatterns) {
userSelect.add(new Option(name, name));
}
// Set current values from 'currentRule' object
ruleDiv.querySelector('.play-term').value = currentRule.playConditionTerm;
ruleDiv.querySelector('.play-bet-type').value = currentRule.playConditionBetType;
ruleDiv.querySelector('.net-gain-condition').value = currentRule.netGainCondition;
ruleDiv.querySelector('.multiplier-condition').value = currentRule.multiplierCondition;
ruleDiv.querySelector('.action-type').value = currentRule.action;
// Set action value based on action type
const riskSelect = ruleDiv.querySelector('.action-value-risk');
if (currentRule.action === 'changeTilesPreset') {
presetSelect.value = currentRule.actionValue;
} else if (currentRule.action === 'changeTilesUser') {
userSelect.value = currentRule.actionValue;
} else if (currentRule.action === 'setRiskLevel') {
riskSelect.value = currentRule.actionValueRisk; // Use separate field
} else if (currentRule.action === 'notify') {
ruleDiv.querySelector('.action-value-text').value = currentRule.actionValue;
}
// 'changeSeed', 'stop', 'changeTilesRandom' have no value field
const updateVisibility = () => {
// Update condition visibility
const type = ruleDiv.querySelector(`input[name="${ruleId}-type"]:checked`)?.value || 'bets';
ruleDiv.querySelector('.play-condition-sentence').style.display = type === 'bets' ? 'flex' : 'none';
ruleDiv.querySelector('.net-gain-condition-sentence').style.display = type === 'profit' ? 'flex' : 'none';
ruleDiv.querySelector('.multiplier-condition-sentence').style.display = type === 'multiplier' ? 'flex' : 'none';
// Update action value visibility
const actionType = ruleDiv.querySelector('.action-type').value;
ruleDiv.querySelector('.action-value-preset').style.display = actionType === 'changeTilesPreset' ? 'block' : 'none';
ruleDiv.querySelector('.action-value-user').style.display = actionType === 'changeTilesUser' ? 'block' : 'none';
ruleDiv.querySelector('.action-value-risk').style.display = actionType === 'setRiskLevel' ? 'block' : 'none';
ruleDiv.querySelector('.action-value-text').style.display = actionType === 'notify' ? 'block' : 'none';
updateSummary();
};
const updateSummary = () => {
const summary = ruleDiv.querySelector('.strategy-rule-summary');
if (!summary) return; // Exit if element not found
const type = ruleDiv.querySelector(`input[name="${ruleId}-type"]:checked`)?.value || 'bets';
let conditionText = '';
try {
if (type === 'bets') {
const termEl = ruleDiv.querySelector('.play-term');
const valueEl = ruleDiv.querySelector('.play-value');
const betTypeEl = ruleDiv.querySelector('.play-bet-type');
conditionText = `On ${termEl.options[termEl.selectedIndex]?.textContent || '?'} ${valueEl.value} ${betTypeEl.options[betTypeEl.selectedIndex]?.textContent || '?'}`;
} else if (type === 'profit') {
const opEl = ruleDiv.querySelector('.net-gain-condition');
const valueEl = ruleDiv.querySelector('.net-gain-value');
conditionText = `If Net Gain ${opEl.options[opEl.selectedIndex]?.textContent || '?'} ${valueEl.value}`;
} else if (type === 'multiplier') {
const opEl = ruleDiv.querySelector('.multiplier-condition');
const valueEl = ruleDiv.querySelector('.multiplier-value');
conditionText = `If Multiplier ${opEl.options[opEl.selectedIndex]?.textContent || '?'} ${valueEl.value}x`;
}
} catch (e) {
console.error("Error updating condition summary:", e);
conditionText = "Error";
}
const actionTypeSelect = ruleDiv.querySelector('.action-type');
const actionText = actionTypeSelect.options[actionTypeSelect.selectedIndex]?.textContent || '?';
let actionValueText = '';
try {
const actionType = actionTypeSelect.value;
if (actionType === 'changeTilesPreset') {
const presetSelectEl = ruleDiv.querySelector('.action-value-preset');
actionValueText = ` to ${presetSelectEl.options[presetSelectEl.selectedIndex]?.textContent || 'N/A'}`;
} else if (actionType === 'changeTilesUser') {
const userSelectEl = ruleDiv.querySelector('.action-value-user');
actionValueText = ` to ${userSelectEl.value || 'N/A'}`;
} else if (actionType === 'setRiskLevel') {
const riskSelectEl = ruleDiv.querySelector('.action-value-risk');
actionValueText = ` to ${riskSelectEl.options[riskSelectEl.selectedIndex]?.textContent || 'N/A'}`;
} else if (actionType === 'notify') {
actionValueText = ` "${ruleDiv.querySelector('.action-value-text')?.value || ''}"`;
}
// Actions like 'stop', 'changeSeed', 'changeTilesRandom' have no specific value text
} catch (e) {
console.error("Error updating action summary:", e);
actionValueText = " Error";
}
summary.innerHTML = `<span>${conditionText} →</span><span class="summary-action">${actionText}${actionValueText}</span>`;
};
// Add event listeners
const header = ruleDiv.querySelector('.strategy-rule-header');
if (header) {
header.addEventListener('click', (e) => {
// Prevent toggling when clicking buttons
if (!e.target.matches('.move-btn, .remove-btn')) {
ruleDiv.classList.toggle('expanded');
updateSummary(); // Update summary when toggling
}
});
}
// Update visibility and summary on any input/select change within the editor
ruleDiv.querySelectorAll('.strategy-rule-editor input, .strategy-rule-editor select').forEach(el => {
el.addEventListener('change', updateVisibility);
el.addEventListener('input', updateSummary); // Also update summary on input for text/number fields
});
const removeBtn = ruleDiv.querySelector('.remove-btn');
if (removeBtn) {
removeBtn.addEventListener('click', () => {
ruleDiv.remove();
// If no rules left, add a default one back? Or handle in saveStrategy
});
}
ruleDiv.querySelectorAll('.move-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent header click toggle
const direction = e.target.dataset.direction;
const parent = ruleDiv.parentNode;
if (direction === 'up' && ruleDiv.previousElementSibling) {
parent.insertBefore(ruleDiv, ruleDiv.previousElementSibling);
} else if (direction === 'down' && ruleDiv.nextElementSibling) {
parent.insertBefore(ruleDiv.nextElementSibling, ruleDiv);
}
});
});
// Initial setup
updateVisibility(); // Set initial visibility based on currentRule
}
function saveStrategy() {
const nameInput = document.getElementById('strategy-name');
const originalName = nameInput.dataset.originalName;
const newName = nameInput.value.trim();
if (!newName) {
showNotification('Strategy name cannot be empty.', "error");
return;
}
if (newName !== originalName && advancedStrategies[newName]) {
showNotification('A strategy with this name already exists.', "error");
return;
}
const rules = [];
let ruleError = false;
document.querySelectorAll('.strategy-rule').forEach((ruleDiv, index) => {
if (ruleError) return; // Stop processing if error found
const action = ruleDiv.querySelector('.action-type').value;
let actionValue = ''; // Default for actions without specific value
let actionValueRisk = 'medium'; // Default for setRiskLevel
try {
if (action === 'changeTilesPreset') {
actionValue = ruleDiv.querySelector('.action-value-preset').value;
} else if (action === 'changeTilesUser') {
actionValue = ruleDiv.querySelector('.action-value-user').value;
if (!actionValue) throw new Error(`User Pattern not selected for rule ${index + 1}`);
} else if (action === 'setRiskLevel') {
actionValueRisk = ruleDiv.querySelector('.action-value-risk').value;
} else if (action === 'notify') {
actionValue = ruleDiv.querySelector('.action-value-text').value;
}
rules.push({
conditionType: ruleDiv.querySelector('input[name*="-type"]:checked').value,
playConditionTerm: ruleDiv.querySelector('.play-term').value,
playConditionValue: parseFloat(ruleDiv.querySelector('.play-value').value) || 1,
playConditionBetType: ruleDiv.querySelector('.play-bet-type').value,
netGainCondition: ruleDiv.querySelector('.net-gain-condition').value,
netGainValue: parseFloat(ruleDiv.querySelector('.net-gain-value').value) || 0,
multiplierCondition: ruleDiv.querySelector('.multiplier-condition').value,
multiplierValue: parseFloat(ruleDiv.querySelector('.multiplier-value').value) || 10,
action: action,
actionValue: actionValue, // Store preset/user pattern name or notify text
actionValueRisk: actionValueRisk // Store risk level separately
});
} catch (e) {
showNotification(`Error saving rule ${index + 1}: ${e.message}`, "error");
ruleError = true;
}
});
if (ruleError) return; // Don't save if errors occurred
if (rules.length === 0) {
showNotification('Strategy must have at least one rule.', "error");
return;
}
if (originalName && originalName !== newName) {
delete advancedStrategies[originalName];
}
advancedStrategies[newName] = rules;
saveStrategies();
document.getElementById('strategy-modal').style.display = 'none';
log(`Strategy '${newName}' saved.`, "system");
}
// ... (editSelectedStrategy, deleteSelectedStrategy remain the same) ...
function editSelectedStrategy() {
const selectedName = document.getElementById('advanced-strategy-list').value;
if (selectedName) {
openStrategyModal(selectedName);
} else {
showNotification('Please select a strategy to edit.', "error");
}
}
function deleteSelectedStrategy() {
const selectedName = document.getElementById('advanced-strategy-list').value;
if (selectedName && confirm(`Are you sure you want to delete the strategy "${selectedName}"?`)) {
delete advancedStrategies[selectedName];
saveStrategies();
log(`Strategy '${selectedName}' deleted.`, "system");
}
}
async function executeAdvancedStrategy(rules) {
const currentStrategyName = document.getElementById('auto-strategy-select').value || document.getElementById('manual-strategy-select').value;
let actionTaken = false; // Flag to ensure only one action per bet cycle if needed
let tileActionTaken = false; // Flag if a tile-changing action was performed
// Reset persistent trigger flags if their conditions are no longer met
for (const ruleKey in botState.persistentTriggerStates) {
const [strategyName, ruleIndexStr] = ruleKey.split('-');
const ruleIndex = parseInt(ruleIndexStr, 10);
if (strategyName !== currentStrategyName || !advancedStrategies[strategyName] || !advancedStrategies[strategyName][ruleIndex]) {
delete botState.persistentTriggerStates[ruleKey];
continue;
}
const rule = advancedStrategies[strategyName][ruleIndex];
const isConditionMet = checkCondition(rule);
if (!isConditionMet) {
delete botState.persistentTriggerStates[ruleKey];
log(`Persistent trigger for Rule #${ruleIndex + 1} reset.`, "strategy");
}
}
for (const [index, rule] of rules.entries()) {
if (actionTaken) break; // Optional: Stop after first matching rule action
const conditionMet = checkCondition(rule);
if (conditionMet) {
log(`<b>Strategy Trigger:</b> Rule #${index + 1}`, "strategy");
const isPersistentCondition = ['profit', 'multiplier'].includes(rule.conditionType);
const ruleKey = `${currentStrategyName}-${index}`;
if (isPersistentCondition) {
if (botState.persistentTriggerStates[ruleKey]) {
log(` ↳ Action skipped: Persistent trigger already fired.`, "strategy");
continue; // Skip this rule, check next one
}
botState.persistentTriggerStates[ruleKey] = true; // Mark as fired for this condition met cycle
}
let currentActionSuccess = true; // Assume success unless action fails
switch (rule.action) {
case 'changeTilesRandom':
log(` ↳ Action: Changing tiles to Random.`, "strategy");
currentActionSuccess = await pickAndApplyRandomPattern();
tileActionTaken = true;
break;
case 'changeTilesPreset':
const [pKey, posKey] = rule.actionValue.split(':');
const presetPattern = patterns[pKey]?.[posKey];
if (presetPattern) {
log(` ↳ Action: Changing tiles to Preset ${pKey.replace('_',' ')} - ${posKey}.`, "strategy");
currentActionSuccess = await pickTilesByNumbers(presetPattern);
} else {
log(` ↳ Action Failed: Preset pattern ${rule.actionValue} not found.`, "error");
currentActionSuccess = false;
}
tileActionTaken = true;
break;
case 'changeTilesUser':
const userPattern = userPatterns[rule.actionValue];
if (userPattern) {
log(` ↳ Action: Changing tiles to User Pattern "${rule.actionValue}".`, "strategy");
// Apply base frame, not animation
currentActionSuccess = await pickTilesByNumbers(userPattern.tiles);
} else {
log(` ↳ Action Failed: User pattern "${rule.actionValue}" not found.`, "error");
currentActionSuccess = false;
}
tileActionTaken = true;
break;
case 'setRiskLevel':
log(` ↳ Action: Setting risk level to ${rule.actionValueRisk}.`, "strategy");
currentActionSuccess = await setRiskLevel(rule.actionValueRisk);
break;
case 'changeSeed':
log(` ↳ Action: Changing client seed.`, "strategy");
// Note: changeSeedProgrammatically handles its own logging/status
currentActionSuccess = await changeSeedProgrammatically();
break;
case 'stop':
log(` ↳ Action: Stopping autoplay.`, "strategy");
if (botState.running) toggleBot();
// No need to set currentActionSuccess = false, stopping is intentional
actionTaken = true; // Ensure loop terminates
return false; // Signal to stop the autoBetLoop
case 'notify':
log(` ↳ Action: Sent notification: "${rule.actionValue}"`, "strategy");
showNotification(rule.actionValue);
// Notification doesn't affect betting flow, don't set actionTaken=true unless intended
break;
}
actionTaken = true; // Indicate an action was attempted or performed
// If the action failed (like picking tiles), maybe stop the bot?
if (!currentActionSuccess) {
log("Strategy action failed. Stopping bot.", "error");
if (botState.running) toggleBot();
return false; // Signal failure
}
// If only one action per cycle is desired, uncomment the break:
// break;
}
}
// If NO strategy rule was met OR no rule changed the tiles,
// we must ensure the *current* pattern (animated or static) is applied.
if (!tileActionTaken) {
const patternSet = await applyCurrentPattern();
if (!patternSet) {
log("Failed to apply default pattern after strategy check. Stopping bot.", "error");
if (botState.running) toggleBot();
return false; // Signal failure
}
return true; // Signal success (default pattern applied)
}
return true; // Indicate strategy execution completed (successfully or intentionally stopped)
}
// ... (checkCondition, saveStrategies, loadStrategies remain similar) ...
function checkCondition(rule) {
switch (rule.conditionType) {
case 'bets': {
const value = rule.playConditionValue;
const betType = rule.playConditionBetType;
const isWin = betType === 'win';
const streak = isWin ? botState.winStreak : botState.lossStreak;
switch (rule.playConditionTerm) {
case 'every':
if (betType === 'bet' && botState.betCount > 0 && botState.betCount % value === 0) return true;
// Trigger on the bet *after* the streak hits the multiple
if (betType === 'win' && botState.lastBetWasWin === false && botState.winStreak > 0 && botState.winStreak % value === 0) return true; // Just ended a win streak of multiple
if (betType === 'lose' && botState.lastBetWasWin === true && botState.lossStreak > 0 && botState.lossStreak % value === 0) return true; // Just ended a loss streak of multiple
break;
case 'everyStreakOf':
// Trigger on the bet *after* the streak hits the multiple
if (betType === 'win' && botState.lastBetWasWin === false && botState.winStreak > 0 && botState.winStreak % value === 0) return true; // Just ended a win streak of multiple
if (betType === 'lose' && botState.lastBetWasWin === true && botState.lossStreak > 0 && botState.lossStreak % value === 0) return true; // Just ended a loss streak of multiple
break;
case 'firstStreakOf':
const flag = `firstStreak-${rule.playConditionBetType}-${rule.playConditionValue}`;
// Check if the streak *just hit* the target value
let justHitStreak = false;
if (betType === 'win' && botState.lastBetWasWin === true && streak === value) justHitStreak = true;
if (betType === 'lose' && botState.lastBetWasWin === false && streak === value) justHitStreak = true;
if (justHitStreak && !botState.strategyFlags[flag]) {
botState.strategyFlags[flag] = true; // Mark as triggered
return true;
}
// Reset flag if streak broken or goes beyond target value
if ((betType === 'win' && !botState.lastBetWasWin) || (betType === 'lose' && botState.lastBetWasWin)) {
// Check all flags related to this bet type and reset if streak is broken
Object.keys(botState.strategyFlags).forEach(key => {
if (key.startsWith(`firstStreak-${rule.playConditionBetType}-`)) {
botState.strategyFlags[key] = false;
}
});
}
break;
case 'streakGreaterThan':
// Trigger if current streak is already greater
if (betType !== 'bet' && streak > value) return true;
break;
}
break;
}
case 'profit':
if (rule.netGainCondition === 'greater' && botState.totalProfit > rule.netGainValue) return true;
if (rule.netGainCondition === 'less' && botState.totalProfit < rule.netGainValue) return true;
if (rule.netGainCondition === '=' && botState.totalProfit.toFixed(8) == rule.netGainValue.toFixed(8)) return true; // Use comparison with tolerance if needed
break;
case 'multiplier':
if (botState.lastMultiplier <= 0) return false; // Only trigger if last bet had a multiplier (was a win)
if (rule.multiplierCondition === 'gte' && botState.lastMultiplier >= rule.multiplierValue) return true;
if (rule.multiplierCondition === 'lte' && botState.lastMultiplier <= rule.multiplierValue) return true;
if (rule.multiplierCondition === 'eq' && botState.lastMultiplier === rule.multiplierValue) return true; // Use comparison with tolerance if needed
break;
}
return false;
}
function saveStrategies() {
localStorage.setItem('stakeKenoBotStrategies', JSON.stringify(advancedStrategies));
populateStrategyDropdowns();
}
function loadStrategies() {
const saved = localStorage.getItem('stakeKenoBotStrategies');
if (saved) {
try {
advancedStrategies = JSON.parse(saved);
// Basic validation: ensure it's an object
if (typeof advancedStrategies !== 'object' || advancedStrategies === null) {
throw new Error("Invalid format: not an object.");
}
// Optional: Deeper validation of rules structure if needed
} catch (e) {
log(`Failed to load strategies: ${e.message}. Resetting to empty.`, "error");
console.error("Failed to load strategies:", e);
advancedStrategies = {};
localStorage.removeItem('stakeKenoBotStrategies'); // Clear invalid data
}
} else {
advancedStrategies = {}; // Initialize if not found
}
}
function populateStrategyDropdowns() {
const autoSelect = document.getElementById('auto-strategy-select');
const manualSelect = document.getElementById('manual-strategy-select'); // Get new element
const advancedList = document.getElementById('advanced-strategy-list');
const rulePresetSelects = document.querySelectorAll('.action-value-preset'); // For presets in rules
const ruleUserSelects = document.querySelectorAll('.action-value-user'); // For user patterns in rules
// Preserve selections
const selectedAuto = autoSelect.value;
const selectedManual = manualSelect ? manualSelect.value : 'none'; // Get new value
const selectedAdvanced = advancedList.value;
// Clear existing options (keep the "-- Select --" or similar default option)
advancedList.innerHTML = '';
while (autoSelect.options.length > 1) autoSelect.remove(1);
if (manualSelect) {
while (manualSelect.options.length > 1) manualSelect.remove(1); // Clear new select
}
rulePresetSelects.forEach(sel => sel.innerHTML = '');
ruleUserSelects.forEach(sel => sel.innerHTML = '');
// Populate Strategy Lists/Selects
for (const name in advancedStrategies) {
advancedList.add(new Option(name, name));
autoSelect.add(new Option(name, name));
if (manualSelect) manualSelect.add(new Option(name, name)); // Add to new select
}
// Populate Preset Patterns in Rule Modals
for (const pKey in patterns) {
for (const posKey in patterns[pKey]) {
const name = `${pKey.replace(/_/g, ' ')} - ${posKey}`;
const value = `${pKey}:${posKey}`;
rulePresetSelects.forEach(sel => sel.add(new Option(name, value)));
}
}
// Populate User Patterns in Rule Modals
for (const name in userPatterns) {
ruleUserSelects.forEach(sel => sel.add(new Option(name, name)));
}
// Restore previous selections if they still exist
autoSelect.value = (selectedAuto === 'none' || advancedStrategies[selectedAuto]) ? selectedAuto : 'none';
if (manualSelect) manualSelect.value = (selectedManual === 'none' || advancedStrategies[selectedManual]) ? selectedManual : 'none'; // Restore new select
advancedList.value = advancedStrategies[selectedAdvanced] ? selectedAdvanced : '';
// If the advanced list is empty, add a disabled placeholder
if (advancedList.options.length === 0) {
advancedList.add(new Option("No strategies created", ""));
advancedList.options[0].disabled = true;
}
}
// ... (exportSelectedStrategy, importStrategy remain the same) ...
function exportSelectedStrategy() {
const selectedName = document.getElementById('advanced-strategy-list').value;
if (!selectedName || !advancedStrategies[selectedName]) {
showNotification("Please select a valid strategy to export.", "error");
return;
}
const strategyData = { name: selectedName, rules: advancedStrategies[selectedName] };
const blob = new Blob([JSON.stringify(strategyData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${selectedName}.keno-strategy.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
log(`Strategy '${selectedName}' exported.`, "system");
}
function importStrategy() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,.keno-strategy'; // Allow specific extension too
input.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = readerEvent => {
try {
const strategyData = JSON.parse(readerEvent.target.result);
// Basic Validation
if (!strategyData.name || typeof strategyData.name !== 'string' || !Array.isArray(strategyData.rules)) {
throw new Error("Invalid strategy file format (missing name or rules array).");
}
// Optional: Add deeper validation of rule structure here if needed
if (advancedStrategies[strategyData.name]) {
if (!confirm(`A strategy named "${strategyData.name}" already exists. Overwrite it?`)) {
return; // User cancelled overwrite
}
}
advancedStrategies[strategyData.name] = strategyData.rules;
saveStrategies(); // Save and repopulate dropdowns
showNotification(`Strategy "${strategyData.name}" imported successfully!`);
log(`Strategy '${strategyData.name}' imported.`, "system");
} catch (err) {
showNotification(`Failed to import strategy: ${err.message}`, "error");
console.error("Import error:", err);
}
};
reader.onerror = () => {
showNotification(`Error reading file: ${reader.error}`, "error");
console.error("File reading error:", reader.error);
}
reader.readAsText(file);
};
input.click();
}
// --- CONFIGURATION MANAGEMENT ---
function populateConfigTab() {
const container = document.getElementById('config-container');
container.innerHTML = ''; // Clear previous content
// Add Auth Token and Endpoint first (if needed for seed change)
const seedChangeKeys = ['authToken', 'seedChangeEndpoint'];
seedChangeKeys.forEach(key => {
if (key in CONFIG) { // Check if key exists in CONFIG
const friendlyName = CONFIG_FRIENDLY_NAMES[key] || key;
const group = document.createElement('div');
group.className = 'config-group';
const labelHtml = `<label for="config-${key}">${friendlyName}</label>`;
let inputWrapperHtml = '';
if (key === 'authToken') {
inputWrapperHtml = `
<div class="input-wrapper">
<input type="password" id="config-${key}" class="bot-input" value="${CONFIG[key]}" placeholder="Auto-detected or paste here">
<button class="config-toggle-vis-btn" data-target="config-${key}" title="Toggle visibility">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</button>
</div>`;
} else {
inputWrapperHtml = `
<div class="input-wrapper">
<input type="text" id="config-${key}" class="bot-input" value="${CONFIG[key]}">
</div>`;
}
group.innerHTML = `${labelHtml}${inputWrapperHtml}`;
container.appendChild(group);
}
});
// Add the rest of the config options
for (const key in CONFIG) {
if (seedChangeKeys.includes(key)) continue; // Skip already added keys
const friendlyName = CONFIG_FRIENDLY_NAMES[key] || key;
const group = document.createElement('div');
group.className = 'config-group';
const labelHtml = `<label for="config-${key}">${friendlyName}</label>`;
let inputWrapperHtml = '';
if (typeof CONFIG[key] === 'boolean') {
inputWrapperHtml = `
<div class="input-wrapper">
<input type="checkbox" id="config-${key}" class="bot-input" ${CONFIG[key] ? 'checked' : ''}>
</div>
`;
} else if (key === 'uiScale') {
inputWrapperHtml = `
<div class="input-wrapper" style="display: flex; align-items: center; gap: 10px;">
<input type="range" id="config-uiScale" class="bot-input" min="50" max="150" value="${CONFIG[key]}" style="flex-grow: 1; padding: 0;">
<span id="config-uiScale-value" style="flex-basis: 40px; text-align: right;">${CONFIG[key]}%</span>
</div>
`;
} else {
const isSelector = !["uiScale", "injectControlsToPage", "autoGames"].includes(key);
inputWrapperHtml = `
<div class="input-wrapper">
<input type="${typeof CONFIG[key] === 'number' ? 'number' : 'text'}" id="config-${key}" class="bot-input" value="${CONFIG[key]}" ${key === 'autoGames' ? 'min="0"' : ''}>
${isSelector ? `<button class="config-pick-btn" data-key="${key}" title="Pick element from page">Pick</button>` : ''}
</div>
`;
}
group.innerHTML = `${labelHtml}${inputWrapperHtml}`;
container.appendChild(group);
}
// Add event listeners after creating elements
container.querySelectorAll('.config-pick-btn').forEach(btn => btn.addEventListener('click', (e) => initSelectorPicker(e.target.dataset.key)));
container.querySelectorAll('.config-toggle-vis-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const targetInput = document.getElementById(e.currentTarget.dataset.target);
if (targetInput) {
const isPassword = targetInput.type === 'password';
targetInput.type = isPassword ? 'text' : 'password';
// Optional: Change icon based on visibility state
}
});
});
const uiScaleSlider = document.getElementById('config-uiScale');
if (uiScaleSlider) {
const uiScaleValue = document.getElementById('config-uiScale-value');
uiScaleSlider.addEventListener('input', () => {
const scale = uiScaleSlider.value;
uiScaleValue.textContent = `${scale}%`;
applyUiScale(scale); // Apply scale change immediately
});
}
}
// ... (initSelectorPicker, generateSelector remain the same) ...
function initSelectorPicker(configKey) {
if (botState.isPickingElement) return;
botState.isPickingElement = true;
const overlay = document.getElementById('element-picker-overlay');
const tooltip = document.getElementById('element-picker-tooltip');
overlay.style.display = 'block';
tooltip.style.display = 'block';
const mouseMoveHandler = (e) => {
try { // Add try-catch for safety during interaction
const target = e.target;
// Don't highlight self or children
if (!target || target.id === 'keno-bot-window' || target.closest('#keno-bot-window') || target.id === 'element-picker-tooltip' || target.id === 'element-picker-overlay') {
overlay.style.display = 'none';
return;
}
overlay.style.display = 'block';
const rect = target.getBoundingClientRect();
Object.assign(overlay.style, {
width: `${rect.width}px`, height: `${rect.height}px`,
top: `${window.scrollY + rect.top}px`, // Account for scrolling
left: `${window.scrollX + rect.left}px` // Account for scrolling
});
} catch (err) {
console.error("Error in mouseMoveHandler:", err);
// Optionally hide overlay on error
// overlay.style.display = 'none';
}
};
const clickHandler = (e) => {
try { // Add try-catch for safety during interaction
e.preventDefault();
e.stopPropagation();
const target = e.target;
if (!target || target.id === 'keno-bot-window' || target.closest('#keno-bot-window') || target.id === 'element-picker-tooltip' || target.id === 'element-picker-overlay') return;
const selector = generateSelector(target);
const inputElement = document.getElementById(`config-${configKey}`);
if (inputElement) {
inputElement.value = selector;
log(`Picked selector for ${configKey}: ${selector}`, "system");
} else {
log(`Error: Could not find config input for ${configKey}`, "error");
}
cleanup(); // Clean up listeners and UI elements
} catch (err) {
console.error("Error in clickHandler:", err);
cleanup(); // Ensure cleanup happens even on error
}
};
const escHandler = (e) => {
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
cleanup();
log("Element picking cancelled.", "system");
}
};
const cleanup = () => {
if (!botState.isPickingElement) return; // Prevent multiple cleanups
document.removeEventListener('mousemove', mouseMoveHandler, true); // Use capture phase
document.removeEventListener('click', clickHandler, true); // Use capture phase
document.removeEventListener('keydown', escHandler, true); // Use capture phase
if (overlay) overlay.style.display = 'none';
if (tooltip) tooltip.style.display = 'none';
botState.isPickingElement = false;
};
// Attach listeners with capture phase to potentially override page listeners
document.addEventListener('mousemove', mouseMoveHandler, true);
document.addEventListener('click', clickHandler, true);
document.addEventListener('keydown', escHandler, true);
}
function generateSelector(el) {
if (!el) return '';
// Prioritize data-testid
const testId = el.getAttribute('data-testid');
if (testId) return `[data-testid="${testId}"]`;
// Fallback to data-test
const dataTest = el.getAttribute('data-test');
if (dataTest) return `[data-test="${dataTest}"]`;
// Fallback to ID if unique enough
if (el.id) {
// Basic check if ID seems specific (avoids generic IDs like 'app')
// You might need more robust checks depending on the site structure
if (!/^(app|main|container|wrapper|root)/i.test(el.id)) {
try {
if (document.querySelectorAll(`#${el.id}`).length === 1) {
return `#${el.id.replace(/:/g, '\\:')}`; // Escape colons if any
}
} catch (e) { /* Invalid ID syntax */ }
}
}
// Fallback to class names, filtering out dynamic/common ones
if (el.className && typeof el.className === 'string') {
const classNames = el.className.split(' ')
.filter(c => c && !/^(svelte-|active|focus|hover)/i.test(c) && c.length > 2) // Filter common/dynamic classes
.map(c => `.${c.replace(/:/g, '\\:')}`) // Escape colons
.join('');
if (classNames) {
try {
// Check if the combination is reasonably specific
if (document.querySelectorAll(classNames).length < 5) { // Adjust threshold as needed
return classNames;
}
} catch (e) { /* Invalid class syntax */ }
}
}
// Absolute fallback: Tag name (least specific)
return el.tagName.toLowerCase();
}
function saveConfig() {
const newConfig = { ...CONFIG };
let configChanged = false;
for (const key in CONFIG) {
const el = document.getElementById(`config-${key}`);
if (el) {
let newValue;
if (typeof CONFIG[key] === 'boolean') {
newValue = el.checked;
} else if (typeof CONFIG[key] === 'number') {
// Use parseFloat for potentially decimal numbers, fallback to 0
newValue = parseFloat(el.value) || 0;
if (key === 'autoGames') newValue = Math.max(0, Math.floor(newValue)); // Ensure non-negative integer
} else { // String
newValue = el.value.trim(); // Trim whitespace from strings
}
// Only update if the value actually changed
if (newConfig[key] !== newValue) {
newConfig[key] = newValue;
configChanged = true;
}
}
}
if (configChanged) {
localStorage.setItem('stakeKenoBotConfig', JSON.stringify(newConfig));
CONFIG = newConfig; // Update the live CONFIG object
showNotification('Configuration saved!');
log("Configuration saved.", "system");
// Handle injection change immediately
const injectedControls = document.getElementById('keno-custom-controls-original');
if (CONFIG.injectControlsToPage && !injectedControls) {
originalInjectControls();
} else if (!CONFIG.injectControlsToPage && injectedControls) {
injectedControls.remove();
log("Removed injected controls from page.", "system");
}
// Re-populate might be needed if selectors changed drastically, but usually not necessary
// populateConfigTab();
} else {
showNotification('No changes detected in configuration.');
}
}
function loadConfig() {
const defaultConfig = {
authToken: '',
seedChangeEndpoint: '/_api/graphql',
betButton: '[data-testid="bet-button"]',
clearButton: '[data-testid="clear-table-button"]',
tile: '[data-testid^="tile-"], .tile.svelte-vebeey', // Keep the updated selector
amountInput: '[data-testid="input-game-amount"]',
riskSelect: '[data-testid="risk-select"]',
pastBetsContainer: '.past-bets', // Using a potentially more stable class selector
injectControlsToPage: false,
uiScale: 100,
autoGames: 0,
};
let loadedConfig = { ...defaultConfig }; // Start with defaults
const saved = localStorage.getItem('stakeKenoBotConfig');
if (saved) {
try {
let customConfig = JSON.parse(saved);
// Merge saved config over defaults, ensuring only known keys are kept
for (const key in defaultConfig) {
if (key in customConfig) {
// Basic type checking (can be expanded)
if (typeof customConfig[key] === typeof defaultConfig[key]) {
loadedConfig[key] = customConfig[key];
} else {
console.warn(`Config key "${key}" has incorrect type in saved data. Using default.`);
}
}
}
// Ensure the 'tile' selector uses the correct default if not saved or invalid
// Also ensure it includes the fix from v3.3.2 if loaded from an older version
if (!loadedConfig.tile || typeof loadedConfig.tile !== 'string' || !loadedConfig.tile.includes('.tile.svelte-vebeey')) {
loadedConfig.tile = defaultConfig.tile;
}
log("Loaded custom config from storage.", "system");
} catch (e) {
log("Failed to parse custom config. Using defaults.", "error");
console.error("Failed to parse custom config from localStorage.", e);
loadedConfig = { ...defaultConfig }; // Reset to default on parse error
localStorage.removeItem('stakeKenoBotConfig'); // Clear corrupted data
}
} else {
log("No saved config found. Using defaults.", "system");
}
// Auto-detect auth token if not loaded or empty
if (!loadedConfig.authToken) {
loadedConfig.authToken = getAuthToken(); // Attempt auto-detection
if (loadedConfig.authToken) {
log("Auto-detected auth token.", "system");
} else {
log("Could not auto-detect auth token. Seed change may fail.", "system");
}
}
CONFIG = loadedConfig; // Assign the final merged/default config
}
function resetConfigToDefault() {
if (confirm("Are you sure you want to reset all configuration to defaults? This will clear saved settings.")) {
localStorage.removeItem('stakeKenoBotConfig');
// Re-call loadConfig to apply the defaults and perform auto-detection again
loadConfig();
populateConfigTab(); // Update UI with defaults
applyUiScale(); // Reset UI scale visual
showNotification("Configuration has been reset to default.");
log("Configuration reset to defaults.", "system");
// Remove injected controls if they exist
const injectedControls = document.getElementById('keno-custom-controls-original');
if (injectedControls) {
injectedControls.remove();
log("Removed injected controls from page.", "system");
}
}
}
// --- SECURITY & ADMIN ---
// ... (hashPassword, loadSecurityConfig, saveCredentials, handleLockUnlock, lockUI, unlockUI, promptForPassword, handleSubmitPassword, updateAdminStatus remain the same) ...
async function hashPassword(password) {
const encoder = new TextEncoder();
const data = encoder.encode(password);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
function loadSecurityConfig() {
botState.storedUsername = localStorage.getItem('stakeKenoBotUser') || '';
botState.storedPasswordHash = localStorage.getItem('stakeKenoBotPassHash') || '';
document.getElementById('admin-username').value = botState.storedUsername;
}
async function saveCredentials() {
const usernameInput = document.getElementById('admin-username');
const passwordInput = document.getElementById('admin-password');
const newUsername = usernameInput.value.trim();
const newPassword = passwordInput.value;
const performSave = async () => {
let changesMade = false;
if (newUsername !== botState.storedUsername) {
botState.storedUsername = newUsername;
localStorage.setItem('stakeKenoBotUser', botState.storedUsername);
log("Username updated.", "system");
changesMade = true;
}
if (newPassword) {
botState.storedPasswordHash = await hashPassword(newPassword);
localStorage.setItem('stakeKenoBotPassHash', botState.storedPasswordHash);
passwordInput.value = ''; // Clear password field after hashing
log("Password updated.", "system");
updateAdminStatus("Password updated. You can now lock the bot.", "info");
changesMade = true;
}
// If no password exists and no new one provided, but username changed
if (!botState.storedPasswordHash && !newPassword && newUsername !== localStorage.getItem('stakeKenoBotUser')) {
changesMade = true; // Still count username change as a change
}
// If password exists, but no new one provided, and username changed
if (botState.storedPasswordHash && !newPassword && newUsername !== localStorage.getItem('stakeKenoBotUser')) {
changesMade = true; // Still count username change as a change
}
if (changesMade) {
showNotification('Credentials saved!');
} else {
showNotification('No changes detected in credentials.');
}
};
// If a password already exists and the user is trying to set a new one OR change username, require current password
if (botState.storedPasswordHash && (newPassword || newUsername !== botState.storedUsername)) {
promptForPassword(performSave, "Enter your CURRENT password to save changes.");
} else {
// Allow setting initial password or changing username without current password if none is set
await performSave();
}
}
function handleLockUnlock() {
if (botState.isLocked) {
promptForPassword(unlockUI);
} else {
if (!botState.storedPasswordHash) {
showNotification("Please set a password before locking.");
return;
}
lockUI();
}
}
function lockUI() {
botState.isLocked = true;
// Force switch to Manual tab upon locking for safety/simplicity
const manualTabBtn = document.querySelector('.bot-tab-btn[data-tab="manual"]');
if (manualTabBtn) manualTabBtn.click();
document.querySelector('.bot-tabs').classList.add('locked');
const lockBtn = document.getElementById('lock-unlock-btn');
lockBtn.textContent = "Unlock";
lockBtn.classList.remove('bot-btn-primary');
lockBtn.classList.add('bot-btn-danger');
updateAdminStatus(`Locked by ${botState.storedUsername || 'Admin'}. UI is restricted.`, "locked");
log("Bot settings locked.", "system");
}
function unlockUI() {
botState.isLocked = false;
document.querySelector('.bot-tabs').classList.remove('locked');
const lockBtn = document.getElementById('lock-unlock-btn');
lockBtn.textContent = "Lock";
lockBtn.classList.remove('bot-btn-danger');
lockBtn.classList.add('bot-btn-primary');
updateAdminStatus("Bot is unlocked. Settings are editable.", "unlocked");
log("Bot settings unlocked.", "system");
}
function promptForPassword(onSuccess, message = "Please enter the password to unlock this feature.") {
const modal = document.getElementById('password-prompt-modal');
if (!modal || modal.style.display === 'flex') return; // Prevent multiple prompts
botState.pendingAction = onSuccess;
const messageEl = document.getElementById('password-prompt-message');
const errorEl = document.getElementById('password-prompt-error');
const inputEl = document.getElementById('prompt-password-input');
if (messageEl) messageEl.textContent = message;
if (inputEl) inputEl.value = ""; // Clear previous input
if (errorEl) errorEl.style.display = 'none'; // Hide previous error
modal.style.display = 'flex';
if (inputEl) inputEl.focus(); // Focus input field
}
async function handleSubmitPassword() {
const passwordInput = document.getElementById('prompt-password-input');
const errorEl = document.getElementById('password-prompt-error');
if (!passwordInput || !errorEl) return;
const password = passwordInput.value;
if (!password) {
errorEl.textContent = "Password cannot be empty.";
errorEl.style.display = 'block';
return;
}
const hash = await hashPassword(password);
if (hash === botState.storedPasswordHash) {
document.getElementById('password-prompt-modal').style.display = 'none';
if (typeof botState.pendingAction === 'function') {
try {
await botState.pendingAction(); // Execute the pending action (could be async)
} catch (e) {
console.error("Error executing pending action after password prompt:", e);
showNotification("An error occurred after unlocking.", "error");
}
}
botState.pendingAction = null; // Clear pending action
} else {
errorEl.textContent = "Incorrect password. Please try again.";
errorEl.style.display = 'block';
passwordInput.focus(); // Re-focus input on error
}
}
function updateAdminStatus(text, status) {
const statusEl = document.getElementById('admin-status');
if (!statusEl) return;
statusEl.textContent = text;
if (status === 'locked') statusEl.style.color = 'var(--accent-red)';
else if (status === 'unlocked') statusEl.style.color = 'var(--accent-green)';
else statusEl.style.color = '#b1bad3'; // Default info color
}
// --- API & SEED CHANGE ---
function getAuthToken() {
// 1. Try specific localStorage keys often used by Stake
const potentialKeys = ['x-access-token', 'token', 'session_token', 'auth_token'];
for (const key of potentialKeys) {
const token = localStorage.getItem(key);
if (token && typeof token === 'string' && token.split('.').length === 3) {
console.log(`Auth token found in localStorage key: "${key}"`);
return token;
}
}
// 2. Try cookies (less common for JWTs but possible)
const cookieToken = document.cookie.split('; ').find(row => row.startsWith('session=') || row.startsWith('token='))?.split('=')[1];
if (cookieToken && cookieToken.split('.').length === 3) {
console.log("Auth token found in cookie.");
return cookieToken;
}
// 3. Brute-force localStorage check (last resort)
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key) continue; // Skip if key is null/undefined
// Avoid checking keys related to the bot itself
if (key.startsWith('stakeKenoBot')) continue;
const value = localStorage.getItem(key);
// Check if it looks like a JWT (string, 3 parts separated by dots)
if (typeof value === 'string' && value.includes('.') && value.split('.').length === 3) {
// Basic check: first part looks like base64 URL encoded JSON (starts with 'ey')
if (value.startsWith('ey')) {
console.log(`Potential auth token found in generic localStorage key: "${key}"`);
return value;
}
}
}
} catch (e) {
console.error("Error iterating localStorage for auth token:", e);
}
console.warn("Could not auto-detect auth token from common locations.");
return ''; // Return empty if not found
}
function generateRandomSeed() {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const length = Math.floor(Math.random() * (64 - 10 + 1)) + 10; // Ensure min length 10
let seed = '';
for (let i = 0; i < length; i++) {
seed += chars.charAt(Math.floor(Math.random() * chars.length));
}
return seed;
}
async function changeSeedProgrammatically() {
updateStatus("Changing Seed...");
log("Attempting to change seed via API...", "strategy");
const token = CONFIG.authToken;
if (!token) {
updateStatus("Error: Auth Token missing!");
log("Seed change failed: Auth Token missing in config. Cannot perform API request.", "error");
showNotification("Auth Token missing. Seed change failed.", "error");
return false;
}
const newClientSeed = generateRandomSeed();
const graphqlQuery = {
query: `
mutation changeClientSeed($seed: String!) {
changeClientSeed(seed: $seed) {
... on ClientSeed { id seed __typename }
... on Error { message __typename }
}
}
`,
variables: { seed: newClientSeed },
operationName: "changeClientSeed"
};
try {
const response = await fetch(CONFIG.seedChangeEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-access-token': token,
// 'Accept': 'application/json' // Good practice
},
body: JSON.stringify(graphqlQuery),
});
const contentType = response.headers.get("content-type");
if (!response.ok || !contentType || !contentType.includes("application/json")) {
const responseText = await response.text();
// Basic check for Cloudflare or HTML response
if (responseText.includes("Cloudflare") || responseText.toLowerCase().includes("<!doctype html")) {
if (!botState.apiBlockStartTime) {
botState.apiBlockStartTime = Date.now();
log("API request blocked (Cloudflare suspected). Cooldown timer started.", "error");
}
throw new Error(`API Request Blocked (Status ${response.status}). Check Cloudflare or network issues.`);
}
throw new Error(`API Error: Status ${response.status}. Expected JSON response.`);
}
const responseData = await response.json();
if (responseData.errors) {
throw new Error(`GraphQL Error: ${responseData.errors.map(e => e.message).join(', ')}`);
}
if (responseData.data?.changeClientSeed?.__typename === 'Error') {
throw new Error(`API Error: ${responseData.data.changeClientSeed.message}`);
}
if (!responseData.data?.changeClientSeed?.seed) {
throw new Error("API Error: Seed change response format unexpected.");
}
// Success
console.log("Successfully changed seed via API:", responseData.data.changeClientSeed);
updateStatus("Seed Changed!");
log(`Seed changed successfully via API.`, "strategy");
showNotification("Client seed changed successfully!");
// Reset cooldown timer on success
if (botState.apiBlockStartTime) {
const cooldownDuration = (Date.now() - botState.apiBlockStartTime) / 1000;
log(`API access restored. Cooldown was ${cooldownDuration.toFixed(1)}s.`, "system");
botState.apiBlockStartTime = 0;
}
return true; // Indicate success
} catch (error) {
console.error("Failed to change seed programmatically:", error);
updateStatus("Error: Seed change failed.");
log(`Seed change failed: ${error.message}`, "error");
showNotification(`Seed change failed: ${error.message}`, "error");
// Start cooldown timer if not already started and error indicates blocking
if (error.message.includes("Blocked") && !botState.apiBlockStartTime) {
botState.apiBlockStartTime = Date.now();
log("API block detected. Cooldown timer started.", "error");
}
return false; // Indicate failure
}
}
async function handleManualSeedChange() {
const btn = document.getElementById('manual-change-seed-btn');
if (!btn || btn.disabled) return; // Prevent multiple clicks
btn.disabled = true;
btn.textContent = "Changing...";
const success = await changeSeedProgrammatically();
// No need to reset text immediately, status updates handle feedback
// Just re-enable after a short delay regardless of success/fail
await delay(1000);
btn.textContent = "Change Seed";
btn.disabled = false;
}
// --- HELPER & STATE FUNCTIONS ---
// ... (getStakeBetAmount, showNotification, updateStatus, updateProfitDisplay, updateInputDisabledState, applyLogFilters, log, saveLogToFile remain the same) ...
function getStakeBetAmount() {
const amountInput = document.querySelector(CONFIG.amountInput);
if (amountInput) {
const amount = parseFloat(amountInput.value);
// Return 0 if NaN or less than minimum possible bet (adjust if needed)
return (isNaN(amount) || amount < 0.00000001) ? 0 : amount;
}
log("Could not find amount input element.", "error");
return 0; // Return 0 if input not found
}
function showNotification(message, type = 'info') {
const container = document.getElementById('bot-notification-container');
if (!container) return; // Ensure container exists
const notification = document.createElement('div');
notification.className = 'bot-notification';
notification.innerHTML = `
<div class="bot-notification-header">
<span>${type === 'error' ? 'Error' : 'Bot Notification'}</span>
<button class="close-btn" title="Dismiss">×</button>
</div>
<p>${message}</p>
`;
if (type === 'error') notification.style.backgroundColor = 'var(--accent-red)';
container.appendChild(notification);
// Function to remove the notification with fade-out
const close = () => {
// Prevent running multiple times
if (notification.classList.contains('fade-out')) return;
notification.classList.add('fade-out');
// Remove from DOM after animation
setTimeout(() => notification.remove(), 500);
};
// Add click listener to close button
notification.querySelector('.close-btn').addEventListener('click', close, { once: true }); // Use {once: true}
// Auto-dismiss after 5 seconds
const timeoutId = setTimeout(close, 5000);
// Optional: Clear timeout if manually closed
notification.addEventListener('click', () => clearTimeout(timeoutId), { once: true });
}
function updateStatus(text) {
const el = document.getElementById('bot-status-indicator');
if (el) el.textContent = `- ${text}`;
}
function updateProfitDisplay() {
const el = document.getElementById('profit-loss-display');
if (!el) return;
const profit = botState.totalProfit;
// Format profit to 8 decimal places
el.textContent = `${profit >= 0 ? '+' : ''}${profit.toFixed(8)}`;
el.classList.remove('profit', 'loss');
if (profit > 0.000000005) { // Use a small tolerance for floating point > 0
el.classList.add('profit');
} else if (profit < -0.000000005) { // Use a small tolerance for floating point < 0
el.classList.add('loss');
}
// If profit is effectively zero, no class is added (neutral color)
}
function updateInputDisabledState() {
const isRunning = botState.running;
// More selectively disable elements
document.querySelectorAll(
// Elements always enabled
'#bot-header button, #minimize-btn, #close-btn, #maximize-btn, #close-minimized-btn, #lock-unlock-btn, #tab-admin input, #tab-admin button, #password-prompt-modal button, #password-prompt-modal input'
).forEach(el => el.disabled = false);
document.querySelectorAll(
// Elements disabled when running (unless in admin or password prompt)
'#tab-manual button, #tab-manual input, #tab-manual select, ' +
'#tab-auto input, #tab-auto select, ' + // Exclude start/stop button initially
'#tab-patterns button, #tab-patterns select, ' +
'#tab-advanced button, #tab-advanced select, ' +
'#tab-log button, #tab-log input, ' + // Includes filters
'#tab-config button, #tab-config input, #tab-config select, .config-pick-btn, .config-toggle-vis-btn, ' +
'#pattern-creator-modal button, #pattern-creator-modal input, #pattern-creator-modal select, #keno-pattern-grid div, ' + // Pattern modal elements
'#strategy-modal button, #strategy-modal input, #strategy-modal select' // Strategy modal elements
).forEach(el => {
// Allow interaction if admin tab is active OR if password prompt is shown
const isAdminTabActive = document.getElementById('tab-admin')?.classList.contains('active');
const isPasswordPromptVisible = document.getElementById('password-prompt-modal')?.style.display === 'flex';
// Special handling for start/stop button
if (el.id === 'start-stop-btn') {
el.disabled = false; // Start/stop should always be clickable
}
// Allow admin tab elements always
else if (el.closest('#tab-admin')) {
el.disabled = false;
}
// Disable most things when running
else {
el.disabled = isRunning;
}
});
// Ensure log filters remain clickable even when running
document.querySelectorAll('#log-filters input').forEach(el => el.disabled = false);
}
function applyLogFilters() {
const logContainer = document.getElementById('log-container');
if (!logContainer) return;
let visibleCount = 0;
logContainer.querySelectorAll('p[data-log-type]').forEach(entry => {
const shouldDisplay = logFilters[entry.dataset.logType];
entry.style.display = shouldDisplay ? '' : 'none';
if (shouldDisplay) visibleCount++;
});
// console.log(`Applied log filters. Visible entries: ${visibleCount}`);
}
function log(message, type = 'system') {
const logContainer = document.getElementById('log-container');
if (!logContainer) {
console.log(`[${type}] ${message}`); // Fallback to console if UI not ready
return;
}
const entry = document.createElement('p');
const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false }); // Use 24-hour format
// Sanitize message slightly - replace potential HTML tags if needed
// For basic bolding like `<b>`, we allow it. For complex HTML, strip it.
// entry.innerHTML = `[${timestamp}] ${message.replace(/</g, "<").replace(/>/g, ">")}`; // Basic sanitization
entry.innerHTML = `[${timestamp}] ${message}`; // Allow basic HTML like <b>
entry.dataset.logType = type;
// Apply classes for styling based on type
entry.classList.add(`log-${type}`); // Use classes for styling
// Set initial display based on filters
entry.style.display = logFilters[type] ? '' : 'none';
logContainer.appendChild(entry);
// Keep log entries manageable (e.g., max 500 entries)
const maxLogEntries = 500;
while (logContainer.childElementCount > maxLogEntries) {
logContainer.removeChild(logContainer.firstElementChild);
}
// Also trim the in-memory logEntries array
const cleanMessage = `[${timestamp}] ${message.replace(/<[^>]*>/g, '')}`; // Store clean text
botState.logEntries.push(cleanMessage);
if (botState.logEntries.length > maxLogEntries) {
botState.logEntries.shift(); // Remove oldest entry from array
}
// Scroll to bottom only if the user hasn't scrolled up manually
const isScrolledToBottom = logContainer.scrollHeight - logContainer.clientHeight <= logContainer.scrollTop + 1;
if (isScrolledToBottom) {
logContainer.scrollTop = logContainer.scrollHeight;
}
}
function saveLogToFile() {
if (botState.logEntries.length === 0) {
showNotification("Log is empty, nothing to save.", "info");
return;
}
const blob = new Blob([botState.logEntries.join('\n')], { type: 'text/plain;charset=utf-8' }); // Specify charset
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// Create a more informative filename
const now = new Date();
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}`;
a.download = `stake_keno_bot_log_${timestamp}.txt`;
document.body.appendChild(a); // Append to body to ensure visibility
try {
a.click();
log("Log saved to file.", "system");
} catch (e) {
log("Error saving log file.", "error");
console.error("File save error:", e);
showNotification("Could not save log file.", "error");
} finally {
document.body.removeChild(a); // Clean up the link
URL.revokeObjectURL(url); // Release the object URL
}
}
// ... (makeDraggable, makeResizable remain the same) ...
function makeDraggable(element, handle) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (!element || !handle) return; // Basic check
const dragMouseDown = (e) => {
// Prevent drag on buttons, inputs, selects, resizers etc.
const ignoredTags = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'];
if (ignoredTags.includes(e.target.tagName) || e.target.classList.contains('resizer') || e.target.closest('.window-controls')) {
// console.log("Drag ignored on target:", e.target);
return;
}
e = e || window.event;
e.preventDefault(); // Prevent text selection during drag
// Get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
// Fix for right-aligned elements jumping on first drag
const rect = element.getBoundingClientRect();
if (element.style.right && element.style.right !== 'auto') {
element.style.right = 'auto';
element.style.left = rect.left + 'px';
}
if (element.style.bottom && element.style.bottom !== 'auto') {
element.style.bottom = 'auto';
element.style.top = rect.top + 'px';
}
};
const elementDrag = (e) => {
e = e || window.event;
e.preventDefault();
// Calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// Set the element's new position:
let newTop = element.offsetTop - pos2;
let newLeft = element.offsetLeft - pos1;
// Boundary checks (optional, keeps element within viewport)
const parentRect = document.body.getBoundingClientRect(); // Or specific parent
const elemRect = element.getBoundingClientRect();
newTop = Math.max(0, Math.min(newTop, parentRect.height - elemRect.height));
newLeft = Math.max(0, Math.min(newLeft, parentRect.width - elemRect.width));
element.style.top = newTop + "px";
element.style.left = newLeft + "px";
};
const closeDragElement = () => {
// Stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
};
handle.onmousedown = dragMouseDown;
}
function makeResizable(element) {
const resizers = element.querySelectorAll('.resizer');
if (!resizers || resizers.length === 0) return;
const minimum_size = {
width: parseInt(getComputedStyle(element).minWidth, 10) || 150,
height: parseInt(getComputedStyle(element).minHeight, 10) || 100
};
const maximum_size = {
width: parseInt(getComputedStyle(element).maxWidth, 10) || window.innerWidth,
height: parseInt(getComputedStyle(element).maxHeight, 10) || window.innerHeight
}
let original_width = 0, original_height = 0;
let original_x = 0, original_y = 0; // Element position
let original_mouse_x = 0, original_mouse_y = 0;
resizers.forEach(currentResizer => {
const resizeMouseDown = (e) => {
e.preventDefault(); // Prevent text selection
original_width = parseFloat(getComputedStyle(element, null).getPropertyValue('width').replace('px', ''));
original_height = parseFloat(getComputedStyle(element, null).getPropertyValue('height').replace('px', ''));
original_x = element.getBoundingClientRect().left;
original_y = element.getBoundingClientRect().top;
original_mouse_x = e.pageX;
original_mouse_y = e.pageY;
window.addEventListener('mousemove', resizeMouseMove);
window.addEventListener('mouseup', resizeMouseUp);
};
const resizeMouseMove = (e) => {
const dx = e.pageX - original_mouse_x;
const dy = e.pageY - original_mouse_y;
let newWidth = original_width;
let newHeight = original_height;
let newLeft = original_x;
let newTop = original_y;
// Adjust dimensions and position based on which resizer is dragged
if (currentResizer.classList.contains('resizer-r') || currentResizer.classList.contains('resizer-br') || currentResizer.classList.contains('resizer-tr')) {
newWidth = original_width + dx;
}
if (currentResizer.classList.contains('resizer-l') || currentResizer.classList.contains('resizer-bl') || currentResizer.classList.contains('resizer-tl')) {
newWidth = original_width - dx;
newLeft = original_x + dx; // Adjust left position when resizing from left
}
if (currentResizer.classList.contains('resizer-b') || currentResizer.classList.contains('resizer-br') || currentResizer.classList.contains('resizer-bl')) {
newHeight = original_height + dy;
}
if (currentResizer.classList.contains('resizer-t') || currentResizer.classList.contains('resizer-tr') || currentResizer.classList.contains('resizer-tl')) {
newHeight = original_height - dy;
newTop = original_y + dy; // Adjust top position when resizing from top
}
// Apply constraints
newWidth = Math.max(minimum_size.width, Math.min(newWidth, maximum_size.width));
newHeight = Math.max(minimum_size.height, Math.min(newHeight, maximum_size.height));
// If resizing from left/top pushed against min size, adjust position back
if (newWidth === minimum_size.width && (currentResizer.classList.contains('resizer-l') || currentResizer.classList.contains('resizer-bl') || currentResizer.classList.contains('resizer-tl'))) {
newLeft = original_x + (original_width - minimum_size.width);
}
if (newHeight === minimum_size.height && (currentResizer.classList.contains('resizer-t') || currentResizer.classList.contains('resizer-tr') || currentResizer.classList.contains('resizer-tl'))) {
newTop = original_y + (original_height - minimum_size.height);
}
// Apply styles
element.style.width = newWidth + 'px';
element.style.height = newHeight + 'px';
// Only update position if resizing from top or left handles
if (currentResizer.classList.contains('resizer-l') || currentResizer.classList.contains('resizer-bl') || currentResizer.classList.contains('resizer-tl')) {
element.style.left = newLeft + 'px';
element.style.right = 'auto'; // Ensure right isn't set
}
if (currentResizer.classList.contains('resizer-t') || currentResizer.classList.contains('resizer-tr') || currentResizer.classList.contains('resizer-tl')) {
element.style.top = newTop + 'px';
element.style.bottom = 'auto'; // Ensure bottom isn't set
}
};
const resizeMouseUp = () => {
window.removeEventListener('mousemove', resizeMouseMove);
window.removeEventListener('mouseup', resizeMouseUp);
};
currentResizer.addEventListener('mousedown', resizeMouseDown);
});
}
// ... (applyUiScale, waitForGameAndEnable remain the same) ...
/**
* handleKeydown - REFACTORED to fix input bug
* 1. If user is typing in *any* input, let them type. Only special case is 'Enter' for password.
* 2. If element picker is active, block hotkeys (it has its own 'Escape' listener).
* 3. If a modal is open (and no input focused), block 's'/'b', but allow 'Escape' to close modal.
* 4. If none of the above, process global hotkeys 's' and 'b'.
*/
function handleKeydown(e) {
const activeEl = document.activeElement;
const isInputFocused = activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'SELECT' || activeEl.tagName === 'TEXTAREA');
// 1. If typing in an input, let it type.
if (isInputFocused) {
// Special case: 'Enter' in password prompt
if (e.key === "Enter" && activeEl.id === 'prompt-password-input') {
e.preventDefault(); // Prevent form submission
document.getElementById('password-prompt-submit')?.click();
}
// For 's', 'b', or any other key, just let the input handle it.
return;
}
// 2. If element picker is active, block hotkeys
if (botState.isPickingElement) {
// The picker's 'escHandler' will catch Escape.
// We just want to prevent 's' and 'b' from firing.
return;
}
// 3. If a modal is open (and no input is focused), block 's' and 'b'
const isModalOpen = document.querySelector('.bot-modal-backdrop[style*="display: flex"]') !== null;
if (isModalOpen) {
if (e.key.toLowerCase() === 's' || e.key.toLowerCase() === 'b') {
e.preventDefault();
e.stopPropagation();
}
// Allow Escape to close modals
if (e.key === "Escape") {
const visibleModal = document.querySelector('.bot-modal-backdrop[style*="display: flex"]');
if (visibleModal) {
// Click the first available cancel button
visibleModal.querySelector('#cancel-pattern-btn, #cancel-strategy-btn, #password-prompt-cancel')?.click();
}
}
return; // Block other keybinds
}
// 4. Global keybinds (no input, no picker, no modal)
switch (e.key.toLowerCase()) {
case 's':
e.preventDefault();
document.getElementById('start-stop-btn')?.click();
break;
case 'b':
e.preventDefault();
document.getElementById('manual-bet-btn')?.click();
break;
}
}
function applyUiScale(scaleValue = CONFIG.uiScale) {
const windowEl = document.getElementById('keno-bot-window');
const minimizedEl = document.getElementById('keno-bot-minimized-bar');
const scale = Math.max(50, Math.min(150, scaleValue)) / 100; // Clamp scale between 50% and 150%
// Apply zoom and adjust transform origin for better scaling appearance
const applyZoom = (el, scaleFactor) => {
if (el) {
el.style.zoom = scaleFactor;
// Optional: Adjust transform origin if needed, especially for elements positioned relative to corners
// el.style.transformOrigin = 'top right'; // Example for top-right positioned element
}
}
applyZoom(windowEl, scale);
applyZoom(minimizedEl, scale);
// Update the slider and value display if the function was called directly
const uiScaleSlider = document.getElementById('config-uiScale');
const uiScaleValue = document.getElementById('config-uiScale-value');
if (uiScaleSlider && uiScaleSlider.value !== String(scaleValue)) {
uiScaleSlider.value = scaleValue;
}
if (uiScaleValue) {
uiScaleValue.textContent = `${scaleValue}%`;
}
}
function waitForGameAndEnable() {
let checkAttempts = 0;
const maxAttempts = 30; // Wait up to 30 seconds
const interval = setInterval(() => {
checkAttempts++;
const betButton = document.querySelector(CONFIG.betButton);
const amountInput = document.querySelector(CONFIG.amountInput); // Check for amount input too
const riskSelect = document.querySelector(CONFIG.riskSelect); // Check risk select
const tiles = document.querySelector(CONFIG.tile); // Check for at least one tile
if (betButton && amountInput && riskSelect && tiles) { // Added tile check
clearInterval(interval);
// Ensure inputs/buttons in the bot UI reflect the loaded state
updateInputDisabledState();
// Set initial risk from Keno state (which might load from config later)
setRiskLevel(kenoState.riskLevel);
updateManualButtonStates(); // Update button active states
updateStatus("Ready");
log("Game interface elements found. Bot is active.", "system");
} else if (checkAttempts >= maxAttempts) {
clearInterval(interval);
updateStatus("Error: Game elements not found!");
log("Failed to find essential game elements after timeout (Bet Button, Amount Input, Risk Select, Tiles). Bot may not function correctly. Check Config selectors.", "error");
showNotification("Error: Could not find Keno game elements. Ensure you are on the Keno page and selectors are correct in the Config tab.", "error");
// Keep UI enabled but log the error
updateInputDisabledState(); // Ensure UI is usable to check config
}
}, 1000);
// Initially disable inputs until game is found (or timeout)
updateInputDisabledState();
updateStatus("Initializing...");
}
// --- ORIGINAL INJECT FUNCTION (For config option) ---
function originalInjectControls() {
const betButton = document.querySelector(CONFIG.betButton);
// Ensure parent exists before trying to insert
if (!betButton?.parentNode || document.getElementById('keno-custom-controls-original')) return;
log("Injecting manual controls directly onto page as per config.", "system");
const mainContainer = document.createElement('div');
mainContainer.id = 'keno-custom-controls-original';
// Use more specific classes if available from Stake's UI, otherwise basic layout
mainContainer.className = 'space-y-2 mt-3';
const style = document.createElement('style');
// Use Stake's variable names if known, otherwise define basics
style.textContent = `
#keno-custom-controls-original { padding: 5px; border: 1px solid var(--border-color, #2f4553); border-radius: 4px; }
#keno-custom-controls-original .flex-container { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
#keno-custom-controls-original .base-btn {
flex-grow: 1; basis: 0;
background-color: var(--button-secondary-bg, #2F4553);
color: var(--button-secondary-color, white);
padding: 0.7rem 1rem; /* Adjust padding */
border-radius: var(--radius-sm, 0.125rem);
border: none; /* Add border: none */
font-weight: 600;
font-size: 0.875rem; /* Adjust font size */
cursor: pointer;
text-align: center;
transition: background-color 0.2s ease;
}
#keno-custom-controls-original .base-btn:hover:not(:disabled) { background-color: var(--button-secondary-hover-bg, #405a69); }
#keno-custom-controls-original .base-btn.active { background-color: var(--button-primary-bg, #3b82f6); color: var(--button-primary-color, white); }
#keno-custom-controls-original .base-btn:disabled { opacity: 0.5; cursor: not-allowed; }
#keno-custom-controls-original .risk-select-inject {
flex-grow: 1;
background-color: var(--input-bg, #0F212E);
color: var(--input-color, white);
border: 1px solid var(--input-border-color, #2f4553);
border-radius: var(--radius-sm, 0.125rem);
padding: 0.7rem;
font-size: 0.875rem;
}
`;
// Use document.head for styles
document.head.appendChild(style);
// Group 1: Type Buttons
const group1 = document.createElement('div');
group1.className = 'flex-container';
group1.innerHTML = `
<button class="base-btn" data-category="type" data-value="bar">Bar</button>
<button class="base-btn" data-category="type" data-value="bird">Bird</button>
<button class="base-btn" data-category="type" data-value="random">Random</button>
`;
// Group 2: Position/Flip
const group2 = document.createElement('div');
group2.className = 'flex-container';
group2.innerHTML = `
<button class="base-btn" data-category="flip" style="flex-grow: 0; padding: 0.7rem;">Flip</button>
<button class="base-btn" data-category="position" data-value="left">Left</button>
<button class="base-btn" data-category="position" data-value="middle">Middle</button>
<button class="base-btn" data-category="position" data-value="right">Right</button>
`;
// Group 3: Risk Select
const group3 = document.createElement('div');
group3.className = 'flex-container';
group3.innerHTML = `
<label for="risk-select-inject-id" style="color: var(--text-secondary, #b1bad3); font-size: 0.875rem; margin-right: 5px;">Risk:</label>
<select id="risk-select-inject-id" class="risk-select-inject">
<option value="classic">Classic</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
`;
const riskSelectInjected = group3.querySelector('select');
riskSelectInjected.value = kenoState.riskLevel; // Set initial value
mainContainer.append(group1, group2, group3);
// Insert after the parent of the bet button (usually the controls container)
betButton.parentNode.parentNode.insertBefore(mainContainer, betButton.parentNode.nextSibling);
// --- Add Listeners to Injected Controls ---
const syncAndUpdate = async (category, value) => {
let changed = false;
if (category === 'type') {
if (kenoState.type !== value) { kenoState.type = value; kenoState.userPatternName = null; changed = true; }
// Don't allow deselect from injected controls for simplicity
} else if (category === 'position') {
if (kenoState.position !== value) { kenoState.position = value; changed = true; }
} else if (category === 'flip') {
kenoState.flip = !kenoState.flip; changed = true;
}
// Update active states for injected buttons
mainContainer.querySelectorAll('.base-btn[data-category="type"]').forEach(b => b.classList.toggle('active', kenoState.type === b.dataset.value));
mainContainer.querySelectorAll('.base-btn[data-category="position"]').forEach(b => b.classList.toggle('active', kenoState.position === b.dataset.value));
mainContainer.querySelector('.base-btn[data-category="flip"]')?.classList.toggle('active', kenoState.flip);
// Disable/Enable Position/Flip based on Type
const posFlipDisabled = !['bar', 'bird'].includes(kenoState.type);
mainContainer.querySelectorAll('[data-category="position"], [data-category="flip"]').forEach(b => b.disabled = posFlipDisabled);
if (posFlipDisabled) { // Reset state and visuals if disabled
kenoState.position = null;
kenoState.flip = false;
mainContainer.querySelectorAll('.base-btn[data-category="position"], .base-btn[data-category="flip"]').forEach(b => b.classList.remove('active'));
}
updateManualButtonStates(); // Sync with main bot UI
if (changed) await applyCurrentPattern(); // Apply pattern if state changed
};
// Button listeners
mainContainer.querySelectorAll('button.base-btn').forEach(btn => {
btn.addEventListener('click', () => {
// Ignore clicks if button is disabled
if (btn.disabled) return;
syncAndUpdate(btn.dataset.category, btn.dataset.value);
});
});
// Risk Select listener
riskSelectInjected.addEventListener('change', async (e) => {
const newRisk = e.target.value;
await setRiskLevel(newRisk); // This updates kenoState and Stake UI
updateManualButtonStates(); // Sync main bot UI dropdown
});
// Initial sync of injected controls state
syncAndUpdate(null, null); // Call once to set initial disabled/active states
riskSelectInjected.value = kenoState.riskLevel;
}
// --- WINDOW CONTROLS ---
function minimizeBot() {
document.getElementById('keno-bot-window').style.display = 'none';
document.getElementById('keno-bot-minimized-bar').style.display = 'flex';
}
function maximizeBot() {
document.getElementById('keno-bot-window').style.display = 'flex';
document.getElementById('keno-bot-minimized-bar').style.display = 'none';
}
function closeBot() {
const win = document.getElementById('keno-bot-window');
const min = document.getElementById('keno-bot-minimized-bar');
if (win) win.style.display = 'none';
if (min) min.style.display = 'none';
// Optional: Add a way to reopen? For now, requires refresh.
log("Bot window closed. Refresh the page to reopen.", "system");
}
// --- UTILITIES ---
/**
* More robust click simulation that triggers React's event handlers
* by setting the 'value' property and dispatching 'input'/'change' events
* or by dispatching mousedown/mouseup for buttons.
*/
function simulateClick(element) {
if (!element) return;
// For buttons, a mousedown/mouseup sequence is often more reliable
try {
const downEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window });
const upEvent = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window });
element.dispatchEvent(downEvent);
element.dispatchEvent(upEvent);
// Fallback to click just in case
// element.click();
} catch (e) {
console.error("Simulated click failed:", e);
// Fallback
element.click();
}
}
/**
* Sets the value of an input or select element and dispatches
* the necessary events (input, change) to trigger React's state updates.
*/
function setUIValue(selector, value) {
const element = document.querySelector(selector);
if (!element) {
console.error(`setUIValue: Element not found for selector: ${selector}`);
return false;
}
try {
if (element.tagName === 'SELECT') {
element.value = value;
} else if (element.tagName === 'INPUT') {
// This trick is often needed to make React see the change
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call(element, value);
} else {
console.warn(`setUIValue: Unsupported element type ${element.tagName}`);
element.value = value; // Try anyway
}
// Dispatch events to notify React of the change
const inputEvent = new Event('input', { bubbles: true, cancelable: true });
const changeEvent = new Event('change', { bubbles: true, cancelable: true });
element.dispatchEvent(inputEvent);
element.dispatchEvent(changeEvent);
return true;
} catch (e) {
console.error(`Failed to set UI value for ${selector}:`, e);
return false;
}
}
// --- PATTERN ANIMATION UTILITIES ---
/**
* Gets the 0-indexed row and col for a Keno tile number (1-40)
* Grid is 8 wide (0-7), 5 high (0-4)
*/
function getTileCoords(tileNum) {
const zeroBasedNum = tileNum - 1;
const row = Math.floor(zeroBasedNum / 8);
const col = zeroBasedNum % 8;
return { row, col };
}
/**
* Calculates metadata for a pattern needed for safe shifting.
* @param {number[]} tiles - Array of tile numbers [1-40]
* @returns {object} - { offsets, minRow, minCol, height, width, validAnchorRows, validAnchorCols }
*/
function calculatePatternMetadata(tiles) {
if (!tiles || tiles.length === 0) {
return { offsets: [], minRow: 0, minCol: 0, height: 0, width: 0, validAnchorRows: 4, validAnchorCols: 7 };
}
const coords = tiles.map(getTileCoords);
const minRow = Math.min(...coords.map(c => c.row));
const maxRow = Math.max(...coords.map(c => c.row));
const minCol = Math.min(...coords.map(c => c.col));
const maxCol = Math.max(...coords.map(c => c.col));
const height = maxRow - minRow + 1;
const width = maxCol - minCol + 1;
// Offsets relative to the bounding box's top-left corner
const offsets = coords.map(c => ({ row: c.row - minRow, col: c.col - minCol }));
const validAnchorRows = 5 - height; // Max row index for anchor
const validAnchorCols = 8 - width; // Max col index for anchor
return { offsets, minRow, minCol, height, width, validAnchorRows, validAnchorCols };
}
/**
* Generates a new array of tile numbers based on offsets and a new anchor position.
* @param {object[]} offsets - Array of {row, col} offsets from calculatePatternMetadata
* @param {number} newAnchorRow - The new top-left row (0-indexed) for the bounding box
* @param {number} newAnchorCol - The new top-left col (0-indexed) for the bounding box
* @returns {number[]} - Array of new tile numbers
*/
function shiftPattern(offsets, newAnchorRow, newAnchorCol) {
const newTiles = [];
for (const offset of offsets) {
const newRow = newAnchorRow + offset.row;
const newCol = newAnchorCol + offset.col;
// Convert (row, col) back to tile number (1-40)
const newTileNum = (newRow * 8) + newCol + 1;
newTiles.push(newTileNum);
}
return newTiles.sort((a,b) => a-b);
}
/**
* Executes the next step of a pattern animation.
* @returns {Promise<boolean>} - True if tiles were picked successfully.
*/
async function executePatternAnimation() {
const anim = kenoState.animation;
if (!anim.enabled || !anim.metadata) {
log("Animation execution failed: State or metadata missing.", "error");
return false;
}
const metadata = anim.metadata;
let newTiles = [];
switch (anim.type) {
case 'random': {
const newRow = Math.floor(Math.random() * (metadata.validAnchorRows + 1));
const newCol = Math.floor(Math.random() * (metadata.validAnchorCols + 1));
anim.currentAnchor = { row: newRow, col: newCol };
newTiles = shiftPattern(metadata.offsets, newRow, newCol);
log(`Anim: Random shift to [${newRow}, ${newCol}]`, "strategy");
break;
}
case 'dvd': {
let newRow = anim.currentAnchor.row + anim.currentVelocity.row;
let newCol = anim.currentAnchor.col + anim.currentVelocity.col;
// Bounce row
if (newRow < 0) {
newRow = 0;
anim.currentVelocity.row *= -1;
} else if (newRow > metadata.validAnchorRows) {
newRow = metadata.validAnchorRows;
anim.currentVelocity.row *= -1;
}
// Bounce col
if (newCol < 0) {
newCol = 0;
anim.currentVelocity.col *= -1;
} else if (newCol > metadata.validAnchorCols) {
newCol = metadata.validAnchorCols;
anim.currentVelocity.col *= -1;
}
anim.currentAnchor = { row: newRow, col: newCol };
newTiles = shiftPattern(metadata.offsets, newRow, newCol);
// log(`Anim: DVD bounce to [${newRow}, ${newCol}]`, "strategy");
break;
}
case 'leftRight': {
let newRow = anim.currentAnchor.row; // Row doesn't change
let newCol = anim.currentAnchor.col + anim.currentVelocity.col;
// Bounce col
if (newCol < 0) {
newCol = 0;
anim.currentVelocity.col *= -1;
} else if (newCol > metadata.validAnchorCols) {
newCol = metadata.validAnchorCols;
anim.currentVelocity.col *= -1;
}
anim.currentAnchor = { row: newRow, col: newCol };
newTiles = shiftPattern(metadata.offsets, newRow, newCol);
// log(`Anim: Left-Right to [${newRow}, ${newCol}]`, "strategy");
break;
}
case 'custom': {
if (!anim.customFrames || anim.customFrames.length === 0) {
log("Animation Error: 'Custom' type selected but no frames exist.", "error");
return false;
}
newTiles = anim.customFrames[anim.currentFrame % anim.customFrames.length];
log(`Anim: Custom Frame ${anim.currentFrame % anim.customFrames.length + 1}`, "strategy");
anim.currentFrame++;
break;
}
default: { // Added curly braces here
log(`Unknown animation type: ${anim.type}. Using base pattern.`, "error");
const pattern = userPatterns[anim.patternName];
newTiles = pattern ? pattern.tiles : [];
break; // Ensure break is inside the block
}
}
if (newTiles.length === 0) {
log("Animation resulted in zero tiles. Aborting.", "error");
return false;
}
return await pickTilesByNumbers(newTiles);
}
// --- START ---
// Use DOMContentLoaded for faster injection, fallback to load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(init, 500));
} else {
setTimeout(init, 500); // Already loaded or interactive
}
})();