379 lines
12 KiB
JavaScript
379 lines
12 KiB
JavaScript
// ========== CONFIG ==========
|
|
|
|
const BASE_URL = 'https://hl3countdown.com/api';
|
|
|
|
const ACCOUNTS = [
|
|
{
|
|
name: 'Account 1',
|
|
headers: {
|
|
'Cookie': 'identity=<identity cookie here>',
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
}
|
|
}
|
|
];
|
|
|
|
const FIXED_PRICE = 1000; // Fixed price for buying stocks
|
|
|
|
// ========== HELPERS ==========
|
|
|
|
async function doJsonFetch(url, options = {}) {
|
|
const res = await fetch(url, {
|
|
...options,
|
|
});
|
|
|
|
let bodyText = '';
|
|
try {
|
|
bodyText = await res.text();
|
|
} catch {
|
|
bodyText = '';
|
|
}
|
|
|
|
let json;
|
|
try {
|
|
json = bodyText ? JSON.parse(bodyText) : null;
|
|
} catch (e) {
|
|
json = null;
|
|
}
|
|
|
|
if (!res.ok) {
|
|
throw new Error(
|
|
`HTTP ${res.status} ${res.statusText} for ${url}. Body: ${bodyText}`
|
|
);
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
// Compute quantity from total_score (thousands, capped at 5000)
|
|
function computeQuantity(total_score) {
|
|
if (typeof total_score !== 'number') {
|
|
throw new Error(`total_score is not a number: ${total_score}`);
|
|
}
|
|
const pointsK = Math.floor(total_score / 1000);
|
|
return Math.min(pointsK, 5000);
|
|
}
|
|
|
|
// Try to extract orderId from buy response
|
|
function extractOrderId(buyResponse) {
|
|
if (!buyResponse) return null;
|
|
|
|
if (typeof buyResponse.orderId === 'number' || typeof buyResponse.orderId === 'string') {
|
|
return buyResponse.orderId;
|
|
}
|
|
if (buyResponse.order && (buyResponse.order.id || buyResponse.order.orderId)) {
|
|
return buyResponse.order.id || buyResponse.order.orderId;
|
|
}
|
|
if (buyResponse.id) {
|
|
return buyResponse.id;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ========== PER-ACCOUNT FLOW ==========
|
|
|
|
async function processAccount(account, isFirstRun) {
|
|
const { name, headers } = account;
|
|
console.log(`\n=== Processing ${name} ===`);
|
|
|
|
// 0. Set username on first run
|
|
if (isFirstRun) {
|
|
const accountNumber = name.match(/\d+/)?.[0] || '0';
|
|
const setUsernameUrl = `https://hl3countdown.com/leaderboard/set-username`;
|
|
|
|
const num = parseInt(accountNumber);
|
|
const isOdd = num % 2 === 1;
|
|
const username = isOdd ? "pee" : "poo";
|
|
|
|
const usernameBody = {
|
|
username: username,
|
|
};
|
|
|
|
console.log(`[${name}] POST ${setUsernameUrl} with body`, usernameBody);
|
|
|
|
try {
|
|
const usernameResponse = await doJsonFetch(setUsernameUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
...headers,
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json, text/plain, */*',
|
|
},
|
|
body: JSON.stringify(usernameBody),
|
|
});
|
|
|
|
console.log(`[${name}] ✓ Username set successfully to: ${username}`);
|
|
console.log(`[${name}] Set username response:`, usernameResponse);
|
|
} catch (err) {
|
|
console.error(`[${name}] ✗ Failed to set username:`, err.message || err);
|
|
}
|
|
}
|
|
|
|
// 1. GET leaderboard/user to fetch total_score
|
|
const leaderboardUrl = `${BASE_URL}/leaderboard/user`;
|
|
console.log(`[${name}] GET ${leaderboardUrl}`);
|
|
|
|
const userData = await doJsonFetch(leaderboardUrl, {
|
|
method: 'GET',
|
|
headers,
|
|
});
|
|
|
|
if (userData == null) {
|
|
throw new Error(`[${name}] Empty response from leaderboard/user`);
|
|
}
|
|
|
|
const total_score = userData.total_score;
|
|
console.log(`[${name}] total_score = ${total_score}`);
|
|
|
|
const quantity = computeQuantity(total_score);
|
|
console.log(`[${name}] computed quantity = ${quantity}`);
|
|
|
|
if (quantity <= 0) {
|
|
console.log(`[${name}] quantity <= 0, skipping buy.`);
|
|
return;
|
|
}
|
|
|
|
// 2. POST stocks/hlx/buy
|
|
// If total_score < 1000, use total_score as price; otherwise use FIXED_PRICE
|
|
const price = FIXED_PRICE;
|
|
|
|
const buyUrl = `${BASE_URL}/stocks/hlx/buy`;
|
|
const buyBody = {
|
|
quantity,
|
|
price,
|
|
};
|
|
|
|
console.log(`[${name}] POST ${buyUrl} with body`, buyBody);
|
|
|
|
const buyResponse = await doJsonFetch(buyUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
...headers,
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json, text/plain, */*',
|
|
},
|
|
body: JSON.stringify(buyBody),
|
|
});
|
|
|
|
console.log(`[${name}] Buy response:`, buyResponse);
|
|
|
|
// 3. Extract orderId
|
|
const orderId = extractOrderId(buyResponse);
|
|
|
|
if (!orderId) {
|
|
console.warn(`[${name}] Could not find orderId in buy response, not deleting.`);
|
|
return;
|
|
}
|
|
|
|
// 4. DELETE stocks/orders/buy/{orderId}
|
|
const deleteUrl = `${BASE_URL}/stocks/orders/buy/${orderId}`;
|
|
console.log(`[${name}] DELETE ${deleteUrl}`);
|
|
|
|
const deleteResponse = await doJsonFetch(deleteUrl, {
|
|
method: 'DELETE',
|
|
headers,
|
|
});
|
|
|
|
console.log(`[${name}] Delete response:`, deleteResponse);
|
|
|
|
// 5. Roulette spin 100 times
|
|
const accountNumber = parseInt(name.match(/\d+/)?.[0] || '1');
|
|
const betType = accountNumber % 2 === 1 ? 'red' : 'black';
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
const rouletteBody = {
|
|
bets: [{
|
|
type: betType,
|
|
amount: 1e-17
|
|
}]
|
|
};
|
|
|
|
const rouletteUrl = `https://hl3countdown.com/api/roulette/spin`;
|
|
|
|
console.log(`[${name}] Spin ${i + 1}/100 - POST ${rouletteUrl}`);
|
|
|
|
try {
|
|
const rouletteResponse = await doJsonFetch(rouletteUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
...headers,
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json, text/plain, */*',
|
|
},
|
|
body: JSON.stringify(rouletteBody),
|
|
});
|
|
|
|
// Collect result for table
|
|
if (rouletteResponse) {
|
|
rouletteResults.push({
|
|
account: name,
|
|
accountNumber: accountNumber,
|
|
spinNumber: i + 1,
|
|
winningNumber: rouletteResponse.winningNumber,
|
|
color: rouletteResponse.color,
|
|
betType: rouletteBody.bets[0].type,
|
|
betAmount: rouletteBody.bets[0].amount,
|
|
won: rouletteResponse.bets[0].won,
|
|
payout: rouletteResponse.totalPayout,
|
|
netChange: rouletteResponse.netChange
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error(`[${name}] Spin ${i + 1} failed:`, err.message || err);
|
|
}
|
|
}
|
|
|
|
console.log(`[${name}] Completed 100 spins`);
|
|
}
|
|
|
|
// ========== MAIN ==========
|
|
|
|
// Global array to collect roulette results
|
|
const rouletteResults = [];
|
|
|
|
async function main() {
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const flagFile = path.join(__dirname, '.first_run_complete');
|
|
|
|
const isFirstRun = !fs.existsSync(flagFile);
|
|
|
|
// First, fetch balances for all accounts
|
|
console.log('=== Fetching account balances ===');
|
|
const accountBalances = [];
|
|
|
|
for (const account of ACCOUNTS) {
|
|
try {
|
|
const leaderboardUrl = `${BASE_URL}/leaderboard/user`;
|
|
const userData = await doJsonFetch(leaderboardUrl, {
|
|
method: 'GET',
|
|
headers: account.headers,
|
|
});
|
|
|
|
const total_score = userData?.total_score || 0;
|
|
accountBalances.push({
|
|
account,
|
|
total_score
|
|
});
|
|
console.log(`${account.name}: ${total_score}`);
|
|
} catch (err) {
|
|
console.error(`Failed to fetch balance for ${account.name}:`, err.message);
|
|
accountBalances.push({
|
|
account,
|
|
total_score: 0
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort by balance (highest to lowest)
|
|
accountBalances.sort((a, b) => b.total_score - a.total_score);
|
|
|
|
// Rename accounts: highest = Account 1, lowest = Account 10
|
|
accountBalances.forEach((item, index) => {
|
|
const newNumber = index + 1;
|
|
item.account.name = `Account ${newNumber}`;
|
|
});
|
|
|
|
console.log('\n=== Accounts renamed by balance (highest to lowest) ===');
|
|
accountBalances.forEach(item => {
|
|
console.log(`${item.account.name}: ${item.total_score}`);
|
|
});
|
|
|
|
try {
|
|
await Promise.all(
|
|
ACCOUNTS.map((account) =>
|
|
processAccount(account, isFirstRun).catch((err) => {
|
|
console.error(`Error in ${account.name}:`, err.message || err);
|
|
})
|
|
)
|
|
);
|
|
|
|
if (isFirstRun) {
|
|
fs.writeFileSync(flagFile, new Date().toISOString());
|
|
console.log('\nFirst run complete. Usernames set.');
|
|
}
|
|
|
|
console.log('\nAll accounts processed.');
|
|
|
|
// Display results table
|
|
if (rouletteResults.length > 0) {
|
|
let output = '';
|
|
|
|
output += '\n\n=== ROULETTE RESULTS TABLE (100 SPINS PER ACCOUNT) ===\n';
|
|
output += '┌─────────────┬────────┬───────┬────────────┬───────┬────────────┬─────┬─────────┬────────────┐\n';
|
|
output += '│ Account │ Acc # │ Spin │ Landed On │ Color │ Bet Type │ Won │ Payout │ Net Change │\n';
|
|
output += '├─────────────┼────────┼───────┼────────────┼───────┼────────────┼─────┼─────────┼────────────┤\n';
|
|
|
|
rouletteResults.forEach(result => {
|
|
const account = result.account.padEnd(11);
|
|
const accNum = String(result.accountNumber).padStart(6);
|
|
const spin = String(result.spinNumber).padStart(5);
|
|
const number = String(result.winningNumber).padStart(10);
|
|
const color = result.color.padEnd(5);
|
|
const betType = result.betType.padEnd(10);
|
|
const won = (result.won ? '✓' : '✗').padEnd(3);
|
|
const payout = String(result.payout).padStart(7);
|
|
const netChange = String(result.netChange).padStart(10);
|
|
|
|
output += `│ ${account} │ ${accNum} │ ${spin} │ ${number} │ ${color} │ ${betType} │ ${won} │ ${payout} │ ${netChange} │\n`;
|
|
});
|
|
|
|
output += '└─────────────┴────────┴───────┴────────────┴───────┴────────────┴─────┴─────────┴────────────┘\n';
|
|
|
|
// Summary statistics
|
|
const totalWins = rouletteResults.filter(r => r.won).length;
|
|
const totalLosses = rouletteResults.filter(r => !r.won).length;
|
|
const totalNetChange = rouletteResults.reduce((sum, r) => sum + r.netChange, 0);
|
|
|
|
output += '\n=== SUMMARY ===\n';
|
|
output += `Total Spins: ${rouletteResults.length}\n`;
|
|
output += `Wins: ${totalWins} (${((totalWins / rouletteResults.length) * 100).toFixed(1)}%)\n`;
|
|
output += `Losses: ${totalLosses} (${((totalLosses / rouletteResults.length) * 100).toFixed(1)}%)\n`;
|
|
output += `Total Net Change: ${totalNetChange}\n`;
|
|
|
|
// Number frequency
|
|
output += '\n=== NUMBER FREQUENCY ===\n';
|
|
const numberFreq = {};
|
|
rouletteResults.forEach(r => {
|
|
numberFreq[r.winningNumber] = (numberFreq[r.winningNumber] || 0) + 1;
|
|
});
|
|
|
|
Object.entries(numberFreq)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.forEach(([num, count]) => {
|
|
output += `Number ${num}: ${count} time(s)\n`;
|
|
});
|
|
|
|
// Color distribution
|
|
output += '\n=== COLOR DISTRIBUTION ===\n';
|
|
const colorFreq = {};
|
|
rouletteResults.forEach(r => {
|
|
colorFreq[r.color] = (colorFreq[r.color] || 0) + 1;
|
|
});
|
|
|
|
Object.entries(colorFreq)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.forEach(([color, count]) => {
|
|
output += `${color}: ${count} time(s) (${((count / rouletteResults.length) * 100).toFixed(1)}%)\n`;
|
|
});
|
|
|
|
// Print to console
|
|
console.log(output);
|
|
|
|
// Write to file in results directory
|
|
const resultsDir = path.join(__dirname, 'results');
|
|
if (!fs.existsSync(resultsDir)) {
|
|
fs.mkdirSync(resultsDir, { recursive: true });
|
|
}
|
|
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const filename = path.join(resultsDir, `roulette_results_${timestamp}.txt`);
|
|
fs.writeFileSync(filename, output, 'utf8');
|
|
console.log(`\n✓ Results written to: ${filename}`);
|
|
}
|
|
} catch (err) {
|
|
console.error('Fatal error:', err);
|
|
}
|
|
}
|
|
|
|
main();
|