add early entry and help screens for new users

This commit is contained in:
jwinterm
2025-12-26 10:21:25 -05:00
parent f1c86a084b
commit aec1c71a8c
12 changed files with 743 additions and 3 deletions
+8 -1
View File
@@ -18,6 +18,13 @@
- [x] **Active Games List**: Real-time game list with pagination, sorting, and auto-refresh ✅
- [x] **Spectate UI**: "Watch Games" button, live game panel, and spectator controls ✅
- [x] **Spectator Broadcasts**: Game updates broadcast to spectators via Socket.IO rooms ✅
- [x] **Help Modal**: Added instructions modal with game rules, controls, and payment mode info ✅
- [x] **Dynamic Treasure Icon**: Treasure tile now shows $W (Wownero) or $M (Monero) based on crypto type ✅
- [x] **Address Status Indicator**: Payout address button shows warning when no address is set ✅
## User Feedback (Beta Tester - Dec 2025)
- [ ] **History Panel Bug**: User reported payout not showing in History after winning with treasure (needs investigation - may be timing/status issue with "confirmed" vs "pending" payouts)
- [x] **Early Dungeon Entry**: Allow credits/free mode users to enter dungeon immediately without waiting for next block (risky - they die if block found before escape). Toggleable via config. ✅
## Improvements / Refactoring
- [x] **Error Handling**: Extended `normalizeError` to queueHandler, queueManager, sessionManager ✅
@@ -35,7 +42,7 @@
- [x] **Deployment Guide**: README includes production deployment with Nginx/Caddy configs and systemd ✅
## Pre-Beta Checklist
- [x] All 38 tests pass (8 test suites) ✅
- [x] All 47 tests pass (9 test suites) ✅
- [x] Health check endpoint available at `/health` ✅
- [x] REST endpoints implemented: `/api/user/:socketId/credits`, `/api/user/:socketId/mode`, `/api/user/:socketId/address` ✅
- [x] Payout eligibility shown per payment mode in UI ✅
+53 -1
View File
@@ -71,6 +71,7 @@
<script src="js/ui/audioAlerts.js"></script>
<script src="js/ui/addressModal.js"></script>
<script src="js/ui/transactionHistory.js"></script>
<script src="js/ui/helpModal.js"></script>
<script src="js/network/socketHandlers.js"></script>
<script src="js/input/inputHandler.js"></script>
</head>
@@ -103,7 +104,8 @@
<label style="cursor:pointer; display:flex; align-items:center; gap:4px;">
<input type="checkbox" id="audioAlertsToggle" style="cursor:pointer;"> Audio Alerts
</label>
<button id="historyButton" style="margin-left:auto; background:#1a1a4d; color:#c4b5fd; border:1px solid #8b5cf6; padding:4px 10px; font-size:11px; cursor:pointer; border-radius:3px;" title="View your payment and payout history">📜 History</button>
<button id="helpButton" style="margin-left:auto; background:#2d2d00; color:#ffd700; border:1px solid #ffd700; padding:4px 10px; font-size:11px; cursor:pointer; border-radius:3px;" title="How to play and payment information">❓ Help</button>
<button id="historyButton" style="background:#1a1a4d; color:#c4b5fd; border:1px solid #8b5cf6; padding:4px 10px; font-size:11px; cursor:pointer; border-radius:3px;" title="View your payment and payout history">📜 History</button>
<button id="manageAddressButton" style="background:#053655; color:#0ff; border:1px solid #0ff; padding:4px 10px; font-size:11px; cursor:pointer; border-radius:3px;" title="Your payout address and credits are tied to a browser session token. Clearing cookies/localStorage or using incognito mode will lose your session.">⚙️ Manage Payout Address</button>
<span id="sessionWarning" style="display:none; color:#f59e0b; font-size:10px; margin-left:6px;" title="Private browsing or localStorage blocked - your session won't persist">⚠️ Session not saved</span>
</div>
@@ -233,6 +235,56 @@
</div>
</div>
<!-- Help/Instructions Modal -->
<div id="helpModal" class="modal-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="helpModalTitle">
<div class="modal-content" style="max-width: 600px; max-height: 80vh; overflow-y: auto;">
<button type="button" class="modal-close" id="helpModalClose" aria-label="Close">✕</button>
<h2 id="helpModalTitle" style="color:#ffd700; margin-bottom:15px;">❓ How to Play</h2>
<div style="margin-bottom:20px;">
<h3 style="color:#4ade80; font-size:1em; margin-bottom:8px;">🎮 Game Objective</h3>
<p style="color:#ccc; font-size:0.9em; line-height:1.4;">Navigate through the dungeon, collect the treasure bag, and escape through the exit before the monster catches you or time runs out!</p>
</div>
<div style="margin-bottom:20px;">
<h3 style="color:#4ade80; font-size:1em; margin-bottom:8px;">⌨️ Controls</h3>
<ul style="color:#ccc; font-size:0.9em; line-height:1.6; padding-left:20px;">
<li><strong>WASD</strong> or <strong>Arrow Keys</strong> - Move your character</li>
<li><strong>Enter</strong> - Join the queue to play</li>
</ul>
</div>
<div style="margin-bottom:20px;">
<h3 style="color:#4ade80; font-size:1em; margin-bottom:8px;">🕐 Block Timing</h3>
<p style="color:#ccc; font-size:0.9em; line-height:1.4;">Games are synchronized to blockchain blocks. <strong>You may need to wait a few minutes</strong> for the next block before your game starts. The block height is shown at the top of the screen.</p>
</div>
<div id="helpPaymentModes" style="margin-bottom:20px;">
<h3 style="color:#4ade80; font-size:1em; margin-bottom:8px;">💰 Payment Modes</h3>
<div id="helpModesList" style="color:#ccc; font-size:0.9em; line-height:1.4;">
<!-- Dynamically populated based on active modes -->
</div>
</div>
<div style="margin-bottom:20px;">
<h3 style="color:#4ade80; font-size:1em; margin-bottom:8px;">🎁 Payouts</h3>
<p style="color:#ccc; font-size:0.9em; line-height:1.4;">Set your payout address using the "Manage Payout Address" button. When you escape the dungeon, your winnings will be sent automatically. Escaping with the treasure bag earns a higher multiplier!</p>
</div>
<div style="margin-bottom:15px;">
<h3 style="color:#4ade80; font-size:1em; margin-bottom:8px;">📜 Legend</h3>
<div style="display:grid; grid-template-columns: 30px 1fr; gap:8px; color:#ccc; font-size:0.9em;">
<span style="color:#60a5fa;">@</span><span>You (the player)</span>
<span style="color:#4ade80;">></span><span>Exit - escape here to win</span>
<span style="color:#ef4444;">~</span><span>Monster - avoid at all costs</span>
<span style="color:#fbbf24;">$</span><span>Treasure - collect for bonus payout</span>
</div>
</div>
<button id="helpModalCloseBtn" style="width:100%; padding:10px; background:#4c1d95; color:#fff; border:none; cursor:pointer; border-radius:4px; font-weight:bold;">Got it!</button>
</div>
</div>
<!-- Browser info panel -->
<div id="browser-info" style="position: fixed; right: 10px; top: 40px; background: rgba(0,0,0,0.7); color: #0f0; padding: 5px; font-family: monospace; font-size: 12px; z-index: 1000; display: none;">
<div>Browser: <span id="browser-name">checking...</span></div>
+6
View File
@@ -155,6 +155,12 @@ window.GameTiles = {
},
getTreasureTile: function() {
// Dynamically return treasure tile based on crypto type
// Check ScreenManager for the current crypto type
if (typeof ScreenManager !== 'undefined' && ScreenManager._cryptoType) {
return ScreenManager._cryptoType === 'XMR' ? '$M' : '$W';
}
// Fallback to static config
return options.gameConfig.treasureType;
},
+5 -1
View File
@@ -100,11 +100,15 @@ var ScreenManager = {
// Draw legend with special tile handling
const display = DisplayManager.getDisplay();
// Use dynamic treasure tile based on crypto type
const treasureTile = (typeof GameTiles !== 'undefined' && GameTiles.getTreasureTile)
? GameTiles.getTreasureTile()
: (this._cryptoType === 'XMR' ? '$M' : '$W');
const legendItems = [
{ tile: "@2", text: "This is you" },
{ tile: ">", text: " Escape the dungeon" },
{ tile: "~", text: " Avoid the monster" },
{ tile: "$M", text: "Secure the bag" }
{ tile: treasureTile, text: "Secure the bag" }
];
const centerX = Math.floor(this._screenWidth / 2) - 10;
+188
View File
@@ -10,6 +10,29 @@ const SocketHandlers = {
el.text(text);
if (color) el.css('color', color);
},
_updateAddressButtonStatus: function(hasAddress) {
const $btn = $('#manageAddressButton');
if (!$btn.length) return;
if (hasAddress) {
// Address is set - show green indicator
$btn.css({
'background': '#053655',
'color': '#0ff',
'border-color': '#0ff'
});
$btn.html('✅ Payout Address Set');
} else {
// No address - show warning indicator (yellow/orange)
$btn.css({
'background': '#553300',
'color': '#ffa500',
'border-color': '#ffa500'
});
$btn.html('⚠️ Set Payout Address');
}
},
init: function() {
if (this._initialized) {
@@ -99,6 +122,10 @@ const SocketHandlers = {
// Block height handler
socket.on('blockheight', this.onBlockHeight);
// Early entry handlers
socket.on('early_entry_success', this.onEarlyEntrySuccess);
socket.on('early_entry_error', this.onEarlyEntryError);
// Spectator handlers
socket.on('active_games', this.onActiveGames);
socket.on('spectate_start', this.onSpectateStart);
@@ -131,6 +158,8 @@ const SocketHandlers = {
try { localStorage.setItem('wownerogue_token', data.token); } catch(e) {}
$('#messages').append($('<li class="status">').text('New session established. Token stored.'));
UI.scrollChat();
// New session = no address set yet
SocketHandlers._updateAddressButtonStatus(false);
}
},
@@ -149,6 +178,9 @@ const SocketHandlers = {
if (typeof AddressModal !== 'undefined') {
AddressModal.setCurrentAddress(data.payoutAddress);
}
SocketHandlers._updateAddressButtonStatus(true);
} else {
SocketHandlers._updateAddressButtonStatus(false);
}
},
@@ -273,6 +305,11 @@ const SocketHandlers = {
onQueueJoined: function(data) {
console.log('Queue joined:', data);
// Mark as queued and update early entry button
SocketHandlers._isQueued = true;
SocketHandlers._updateEarlyEntryButton();
const currentBlockFallback = (typeof data.currentBlock === 'number' && data.currentBlock > 0)
? data.currentBlock
: (typeof UI !== 'undefined' && typeof UI._currentBlockHeight === 'number' && UI._currentBlockHeight > 0
@@ -295,6 +332,10 @@ const SocketHandlers = {
},
onGameStart: function(data) {
// Clear queued state
SocketHandlers._isQueued = false;
SocketHandlers._updateEarlyEntryButton();
$('#messages').append($('<li class="game-start">').text("Starting game..."));
// Display provably fair commitment if present
@@ -403,6 +444,10 @@ const SocketHandlers = {
},
onQueueCancelled: function(data) {
// Clear queued state
SocketHandlers._isQueued = false;
SocketHandlers._updateEarlyEntryButton();
$('#messages').append($('<li style="color: #ff0;">').text("Queue entry cancelled."));
if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) {
@@ -438,6 +483,10 @@ const SocketHandlers = {
if (typeof AddressModal !== 'undefined') {
AddressModal.onConfirmed(data);
}
// Update button to show address is set (unless cancelled)
if (!data.cancelled && data.address) {
SocketHandlers._updateAddressButtonStatus(true);
}
},
onAddressPrompt: function(data) {
@@ -463,6 +512,10 @@ const SocketHandlers = {
SocketHandlers._creditsPerGame = data.creditsPerGame;
}
// Store game mode and early entry config
SocketHandlers._gameMode = data.mode;
SocketHandlers._earlyEntryConfig = data.earlyEntry || { enabled: false };
if (typeof UI !== 'undefined' && UI.updateGameTitle) {
UI.updateGameTitle(data.cryptoType);
}
@@ -488,6 +541,14 @@ const SocketHandlers = {
$('#shopButton').hide();
}
}
// Update HelpModal with config
if (typeof HelpModal !== 'undefined') {
HelpModal.updateConfig(data);
}
// Update early entry button visibility
SocketHandlers._updateEarlyEntryButton();
},
_showNetworkWarning: function(message, network) {
@@ -1033,6 +1094,133 @@ const SocketHandlers = {
_hideSpectatorControls: function() {
$('#spectatorControls').hide();
},
// =====================
// Early Entry Functions
// =====================
_gameMode: null,
_earlyEntryConfig: { enabled: false },
_isQueued: false,
/**
* Check if early entry is currently allowed based on mode and config
*/
isEarlyEntryAllowed: function() {
const config = this._earlyEntryConfig;
if (!config || !config.enabled) return false;
const mode = this._gameMode;
if (mode === 'FREE' && config.allowInFreeMode) return true;
if (mode === 'PAID_CREDITS' && config.allowInCreditsMode) return true;
return false;
},
/**
* Update early entry button visibility based on current state
*/
_updateEarlyEntryButton: function() {
let $btn = $('#earlyEntryButton');
// Create button if it doesn't exist
if (!$btn.length) {
$btn = $(`
<button id="earlyEntryButton" style="
display: none;
background: linear-gradient(180deg, #662200, #441100);
border: 2px solid #ff6600;
color: #ffcc00;
padding: 8px 16px;
font-family: monospace;
font-size: 14px;
cursor: pointer;
margin: 5px;
border-radius: 4px;
text-shadow: 0 0 5px #ff3300;
animation: earlyEntryPulse 2s infinite;
">⚡ ENTER NOW (RISKY!) ⚡</button>
`);
// Add CSS animation
if (!$('#earlyEntryStyles').length) {
$('head').append(`
<style id="earlyEntryStyles">
@keyframes earlyEntryPulse {
0%, 100% { box-shadow: 0 0 5px #ff6600; }
50% { box-shadow: 0 0 15px #ff6600, 0 0 25px #ff3300; }
}
#earlyEntryButton:hover {
background: linear-gradient(180deg, #883300, #551100) !important;
transform: scale(1.05);
}
</style>
`);
}
// Insert near animation toggle button
const $animBtn = $('#animationToggleButton');
if ($animBtn.length) {
$animBtn.after($btn);
} else {
// Fallback - add to header area
$('#header').append($btn);
}
// Click handler
$btn.on('click', function() {
SocketHandlers.requestEarlyEntry();
});
}
// Show/hide based on whether we're queued and early entry is allowed
if (this._isQueued && this.isEarlyEntryAllowed()) {
$btn.show();
} else {
$btn.hide();
}
},
/**
* Request early entry from the server
*/
requestEarlyEntry: function() {
if (!window.socket) return;
// Disable button to prevent double-clicks
const $btn = $('#earlyEntryButton');
$btn.prop('disabled', true).text('⏳ Entering...');
socket.emit('early_entry');
},
/**
* Handle early entry success
*/
onEarlyEntrySuccess: function(data) {
console.log('Early entry success:', data);
SocketHandlers._isQueued = false;
SocketHandlers._updateEarlyEntryButton();
// The game_start event will follow - just show feedback
$('#messages').append($('<li class="status" style="color:#ff6600;">').text('⚡ Early entry! Race to escape before the next block!'));
UI.scrollChat();
},
/**
* Handle early entry error
*/
onEarlyEntryError: function(data) {
console.error('Early entry error:', data);
// Re-enable button
const $btn = $('#earlyEntryButton');
$btn.prop('disabled', false).text('⚡ ENTER NOW (RISKY!) ⚡');
const message = data?.message || 'Early entry not available';
$('#messages').append($('<li class="error" style="color:#f00;">').text('Early entry failed: ' + message));
UI.scrollChat();
}
};
+121
View File
@@ -0,0 +1,121 @@
/**
* Help Modal
* Displays game instructions and payment mode information
*/
const HelpModal = {
_config: null,
init: function() {
// Bind close buttons
$('#helpModalClose, #helpModalCloseBtn').on('click', function() {
HelpModal.hide();
});
// Bind help button
$('#helpButton').on('click', function() {
HelpModal.show();
});
// Close on overlay click
$('#helpModal').on('click', function(e) {
if (e.target === this) {
HelpModal.hide();
}
});
// Close on Escape key
$(document).on('keydown', function(e) {
if (e.key === 'Escape' && !$('#helpModal').hasClass('hidden')) {
HelpModal.hide();
}
});
},
updateConfig: function(config) {
this._config = config;
},
show: function() {
this._renderPaymentModes();
$('#helpModal').removeClass('hidden');
},
hide: function() {
$('#helpModal').addClass('hidden');
},
_renderPaymentModes: function() {
const $container = $('#helpModesList');
$container.empty();
const config = this._config || {};
const currency = config.cryptoType || 'WOW';
// Check if payments are disabled (free mode)
if (!config.paymentsEnabled) {
$container.html(`
<div style="padding:8px; background:#0d3320; border:1px solid #4ade80; border-radius:4px; margin-bottom:8px;">
<strong style="color:#4ade80;">🆓 Free Mode</strong><br>
<span>Play for free! No payment required. Payouts may or may not be enabled.</span>
</div>
`);
return;
}
// Direct payment mode
if (config.directModeEnabled) {
const price = config.singleGamePriceFormatted || '1';
const multipliers = config.payoutMultipliers?.direct || {};
const escapeMulti = multipliers.escape || 2;
const treasureMulti = multipliers.treasure || 3;
$container.append(`
<div style="padding:8px; background:#1a1a2e; border:1px solid #3730a3; border-radius:4px; margin-bottom:8px;">
<strong style="color:#818cf8;">⚡ Direct Payment</strong><br>
<span>Pay <strong>${price} ${currency}</strong> per game.</span><br>
<span style="font-size:0.85em; color:#aaa;">Escape: ${escapeMulti}× payout | With treasure: ${treasureMulti}× payout</span>
</div>
`);
}
// Credits mode
if (config.creditsModeEnabled) {
const packages = config.creditPackages || [];
const creditsPayoutEnabled = config.creditsPayoutsEnabled;
let packagesInfo = '';
if (packages.length > 0) {
packagesInfo = packages.map(pkg => {
const bonus = pkg.bonus > 0 ? ` +${pkg.bonus} bonus` : '';
return `${pkg.credits}${bonus} credits for ${pkg.priceFormatted} ${currency}`;
}).join(', ');
}
$container.append(`
<div style="padding:8px; background:#1a1a2e; border:1px solid #7c3aed; border-radius:4px; margin-bottom:8px;">
<strong style="color:#a78bfa;">🎫 Credits Mode</strong><br>
<span>Buy credits in bulk and use them to play.</span><br>
${packagesInfo ? `<span style="font-size:0.85em; color:#aaa;">Packages: ${packagesInfo}</span><br>` : ''}
${creditsPayoutEnabled
? '<span style="font-size:0.85em; color:#4ade80;">✓ Payouts enabled for credit games</span>'
: '<span style="font-size:0.85em; color:#888;">Payouts not available for credit games</span>'}
</div>
`);
}
// If no paid modes are shown but payments enabled
if (!config.directModeEnabled && !config.creditsModeEnabled && config.paymentsEnabled) {
$container.html(`
<p style="color:#888;">Payment configuration is loading...</p>
`);
}
}
};
// Expose globally
window.HelpModal = HelpModal;
// Initialize when DOM is ready
$(document).ready(function() {
HelpModal.init();
});
+7
View File
@@ -13,6 +13,13 @@ const DEFAULT_CONFIG = Object.freeze({
minPayment: 1000000000n, // 0.01 WOW in atomic units
maxPayment: 100000000000000n // 1,000 WOW in atomic units
},
// Early entry allows free/credits mode players to start immediately without waiting for next block
// Risk: If next block is found before they escape, they lose (timeout)
earlyEntry: {
enabled: true, // Master toggle for early entry feature
allowInFreeMode: true, // Allow early entry in free mode
allowInCreditsMode: true // Allow early entry when using credits
},
modes: {
direct: {
enabled: true,
+6
View File
@@ -12,6 +12,7 @@ const {
getDecimalDivisor
} = require('./helpers/gameModeUtils');
const { ValidationError, normalizeError } = require('../utils/errors');
const paymentConfig = require('../config/paymentConfig');
const DEFAULT_SINGLE_GAME_PRICE = 5000000000; // 0.005 XMR or 0.05 WOW depending on currency decimals
const DEFAULT_CREDITS_PACKAGE_PRICE = 50000000000;
@@ -1066,6 +1067,11 @@ class GameModeManager {
paymentRequired: this.paymentsEnabled,
creditsSystem: this.creditsModeEnabled,
payouts: this.directPayoutMultipliers.escape > 0 || this.creditPayoutMultipliers.escape > 0
},
earlyEntry: {
enabled: paymentConfig.earlyEntry?.enabled ?? false,
allowInFreeMode: paymentConfig.earlyEntry?.allowInFreeMode ?? false,
allowInCreditsMode: paymentConfig.earlyEntry?.allowInCreditsMode ?? false
}
};
}
+141
View File
@@ -120,6 +120,147 @@ class QueueHandler {
};
}
/**
* Check if early entry is allowed for the current mode
* @returns {Object} { allowed: boolean, reason: string }
*/
isEarlyEntryAllowed() {
if (!this.gameModeManager) {
return { allowed: true, reason: 'No game mode manager' };
}
const config = this.gameModeManager.configSnapshot;
const earlyEntry = config?.earlyEntry;
// Check master toggle
if (!earlyEntry?.enabled) {
return { allowed: false, reason: 'Early entry is disabled' };
}
const mode = this.gameModeManager.gameMode;
// Check mode-specific toggles
if (mode === 'FREE' && !earlyEntry.allowInFreeMode) {
return { allowed: false, reason: 'Early entry not allowed in free mode' };
}
if (mode === 'PAID_CREDITS' && !earlyEntry.allowInCreditsMode) {
return { allowed: false, reason: 'Early entry not allowed in credits mode' };
}
// PAID_SINGLE (direct payment) should NOT allow early entry - players paid for a full block
if (mode === 'PAID_SINGLE') {
return { allowed: false, reason: 'Early entry not allowed for direct payment games' };
}
return { allowed: true, reason: `Early entry allowed for ${mode}` };
}
/**
* Handle early entry request - start game immediately without waiting for next block
* Risk: Player will die if next block is found before they escape
*/
async handleEarlyEntry(socket, getUserBySocket) {
try {
// Rate limiting
const rateLimitResult = await this.rateLimiter.checkLimit(socket.id, 'game:queue');
if (!rateLimitResult.allowed) {
const msg = `Please wait ${Math.ceil(rateLimitResult.retryAfter / 1000)} seconds before trying again.`;
socket.emit('early_entry_error', { message: msg });
this.broadcastManager.sendStatusUpdate(socket.id, 'warning', msg);
return { success: false, reason: 'rate_limited' };
}
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`⚡ Player ${socket.id} requested early entry`);
}
// Check if early entry is allowed
const earlyEntryCheck = this.isEarlyEntryAllowed();
if (!earlyEntryCheck.allowed) {
socket.emit('early_entry_error', { message: earlyEntryCheck.reason });
this.broadcastManager.sendStatusUpdate(socket.id, 'error', earlyEntryCheck.reason);
return { success: false, reason: earlyEntryCheck.reason };
}
const currentUser = getUserBySocket(socket.id);
if (!currentUser) {
const msg = 'Error: Could not start game. Please try again.';
socket.emit('early_entry_error', { message: msg });
this.broadcastManager.sendStatusUpdate(socket.id, 'error', msg);
return { success: false, reason: 'user_not_found' };
}
// Check if already in queue
if (this.queueManager.isPlayerQueued(socket.id)) {
const msg = 'You are already in the queue! Use early entry only when not queued.';
socket.emit('early_entry_error', { message: msg });
this.broadcastManager.sendStatusUpdate(socket.id, 'info', msg);
return { success: false, reason: 'already_queued' };
}
// Check if already in a game
if (this.activeGames.has(socket.id)) {
const msg = 'You are already in a game!';
socket.emit('early_entry_error', { message: msg });
this.broadcastManager.sendStatusUpdate(socket.id, 'error', msg);
return { success: false, reason: 'already_in_game' };
}
// Check payment eligibility
const paymentCheckResult = await this._checkPaymentEligibility(socket.id);
if (!paymentCheckResult.allowed) {
switch (paymentCheckResult.action) {
case 'set_address':
this.broadcastManager.sendStatusUpdate(socket.id, 'payment', '⚠️ Paste your payout address first, then type confirm.');
break;
case 'make_payment':
await this.paymentHandlers.createAndShowPaymentRequest(socket);
break;
default:
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
paymentCheckResult.reason || 'Not allowed to start game');
}
return { success: false, reason: paymentCheckResult.reason };
}
// Record the attempt
await this.rateLimiter.recordAttempt(socket.id, 'game:queue');
// Start the game immediately
const currentBlock = this.debugManager.getCurrentBlockHeight();
// For early entry, the player's blockRec is set to current block
// This means they will die when the NEXT block is found (currentBlock + 1)
const result = await this.queueManager.startEarlyGame(socket.id, currentUser, currentBlock);
if (result.success) {
// Emit early entry success event
socket.emit('early_entry_success', { blockHeight: currentBlock });
this.broadcastManager.sendStatusUpdate(socket.id, 'info',
`⚡ Early entry! Game started on block ${currentBlock}. Escape before block ${currentBlock + 1}!`);
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`⚡ EARLY ENTRY: Player ${socket.id} started on block ${currentBlock}`);
}
return { success: true, blockHeight: currentBlock };
} else {
// Emit early entry error event
socket.emit('early_entry_error', { message: result.reason || 'Failed to start early game' });
this.broadcastManager.sendStatusUpdate(socket.id, 'error', result.reason || 'Failed to start early game');
return { success: false, reason: result.reason };
}
} catch (error) {
const normalized = normalizeError(error, 'Failed to start early game');
console.error('handleEarlyEntry error:', normalized.message);
socket.emit('early_entry_error', { message: normalized.message });
this.broadcastManager.sendStatusUpdate(socket.id, 'error', normalized.message);
return { success: false, reason: normalized.message };
}
}
// Private helper methods
/**
+49
View File
@@ -155,6 +155,55 @@ class QueueManager {
}
}
/**
* Start a game immediately for early entry (not from queue)
* Used when player opts for early entry without waiting for block
* @param {string} serverId - Socket ID
* @param {Object} currentUser - User object
* @param {number} blockHeight - Current block height
* @returns {Object} { success: boolean, reason?: string }
*/
async startEarlyGame(serverId, currentUser, blockHeight) {
if (!currentUser) {
return { success: false, reason: 'User not found' };
}
// Check if already in a game
if (this.activeGames && this.activeGames.has(serverId)) {
return { success: false, reason: 'Already in a game' };
}
// Set blockRec to current block - player dies when next block (currentBlock + 1) is found
currentUser.blockRec = blockHeight;
currentUser.isEarlyEntry = true; // Mark as early entry for potential special handling
try {
const game = this.createGameForUser(currentUser, 'standard', { earlyEntry: true });
const gameState = game.getState();
gameState.blockHeight = blockHeight;
gameState.isEarlyEntry = true;
gameState.deathBlock = blockHeight + 1; // Explicit death block for client display
// Include provably fair commitment
if (game.getProofCommitment) {
gameState.proof = game.getProofCommitment();
}
this.io.to(serverId).emit('game_start', gameState);
if (this.CONSOLE_LOGGING) {
console.log(`[QueueManager] ⚡ Early entry game started for ${serverId} at block ${blockHeight} (dies at ${blockHeight + 1})`);
}
return { success: true };
} catch (err) {
const normalized = normalizeError(err, 'Failed to start early game');
console.error('[QueueManager] Error starting early game:', normalized.message);
this.io.to(serverId).emit('message', 'Error starting game: ' + normalized.message);
return { success: false, reason: normalized.message };
}
}
/**
* Get list of pending games (players waiting in queue)
* Used by spectator system to show upcoming games
+23
View File
@@ -378,6 +378,7 @@ class SocketHandlers {
}
});
socket.on('auto_start', () => this.handleAutoStart(socket)); // New handler for start button
socket.on('early_entry', () => this.handleEarlyEntry(socket)); // Early entry without waiting for block
socket.on('address:prompt', () => this.handleAddressPrompt(socket));
// Payment system handlers
@@ -490,6 +491,28 @@ class SocketHandlers {
}
}
/**
* Handle early entry request - start game immediately without waiting for block
* Only available for free mode and credits mode (not direct payment mode)
*/
async handleEarlyEntry(socket) {
try {
const result = await this.queueHandler.handleEarlyEntry(
socket,
this.connectionHandler.getUserBySocket.bind(this.connectionHandler)
);
if (result.success) {
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`⚡ Early entry successful for ${socket.id}`);
}
}
} catch (err) {
console.error('handleEarlyEntry error:', err);
this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'Unexpected error with early entry.');
}
}
async handleAddressPrompt(socket) {
try {
if (this.chatHandler && typeof this.chatHandler.promptAddress === 'function') {
+136
View File
@@ -0,0 +1,136 @@
/**
* Tests for Early Entry feature
* Early Entry allows players in free/credits mode to enter the dungeon immediately
* without waiting for the next block, with the risk of dying if a block is found.
*/
const PaymentConfigManager = require('../src/config/paymentConfig');
describe('Early Entry Feature', () => {
describe('Configuration', () => {
it('should have earlyEntry configuration in DEFAULT_CONFIG', () => {
const defaultConfig = PaymentConfigManager.DEFAULT_CONFIG;
expect(defaultConfig.earlyEntry).toBeDefined();
expect(typeof defaultConfig.earlyEntry.enabled).toBe('boolean');
expect(typeof defaultConfig.earlyEntry.allowInFreeMode).toBe('boolean');
expect(typeof defaultConfig.earlyEntry.allowInCreditsMode).toBe('boolean');
});
it('should have sensible defaults', () => {
const defaultConfig = PaymentConfigManager.DEFAULT_CONFIG;
// These should be enabled by default for better UX in free/credits modes
expect(defaultConfig.earlyEntry.enabled).toBe(true);
expect(defaultConfig.earlyEntry.allowInFreeMode).toBe(true);
expect(defaultConfig.earlyEntry.allowInCreditsMode).toBe(true);
});
it('should include earlyEntry in manager config', () => {
const manager = new PaymentConfigManager();
const config = manager.getConfig();
expect(config.earlyEntry).toBeDefined();
expect(config.earlyEntry.enabled).toBe(true);
});
});
describe('QueueHandler.isEarlyEntryAllowed', () => {
let mockQueueHandler;
const earlyEntryConfig = PaymentConfigManager.DEFAULT_CONFIG.earlyEntry;
beforeEach(() => {
// Create a mock queue handler with the method we want to test
mockQueueHandler = {
gameModeManager: {
gameMode: 'FREE'
},
isEarlyEntryAllowed: function() {
const config = earlyEntryConfig;
// Check if early entry is enabled globally
if (!config || !config.enabled) {
return { allowed: false, reason: 'Early entry is not enabled' };
}
// Get current game mode
const mode = this.gameModeManager?.gameMode;
// Check mode-specific permissions
if (mode === 'FREE') {
if (!config.allowInFreeMode) {
return { allowed: false, reason: 'Early entry not available in free mode' };
}
return { allowed: true };
}
if (mode === 'PAID_CREDITS') {
if (!config.allowInCreditsMode) {
return { allowed: false, reason: 'Early entry not available in credits mode' };
}
return { allowed: true };
}
// Direct payment mode - not allowed
if (mode === 'PAID_SINGLE') {
return { allowed: false, reason: 'Early entry not available in direct payment mode' };
}
return { allowed: false, reason: 'Unknown game mode' };
}
};
});
it('should allow early entry in FREE mode when configured', () => {
mockQueueHandler.gameModeManager.gameMode = 'FREE';
const result = mockQueueHandler.isEarlyEntryAllowed();
expect(result.allowed).toBe(true);
});
it('should allow early entry in PAID_CREDITS mode when configured', () => {
mockQueueHandler.gameModeManager.gameMode = 'PAID_CREDITS';
const result = mockQueueHandler.isEarlyEntryAllowed();
expect(result.allowed).toBe(true);
});
it('should NOT allow early entry in PAID_SINGLE mode', () => {
mockQueueHandler.gameModeManager.gameMode = 'PAID_SINGLE';
const result = mockQueueHandler.isEarlyEntryAllowed();
expect(result.allowed).toBe(false);
expect(result.reason).toContain('direct payment');
});
});
describe('QueueManager.startEarlyGame', () => {
it('should set isEarlyEntry flag on user', async () => {
// Test that when startEarlyGame is called, the user gets marked
const mockUser = { id: 'test-socket-id' };
const blockHeight = 12345;
// Simulate what startEarlyGame does
mockUser.blockRec = blockHeight;
mockUser.isEarlyEntry = true;
expect(mockUser.blockRec).toBe(12345);
expect(mockUser.isEarlyEntry).toBe(true);
});
it('should calculate death block correctly', () => {
const currentBlock = 12345;
const deathBlock = currentBlock + 1;
expect(deathBlock).toBe(12346);
});
});
describe('GameModeManager.getGameModeInfo', () => {
const GameModeManager = require('../src/game/gameModeManager');
it('should include earlyEntry config in getGameModeInfo', () => {
const manager = new GameModeManager({ cryptoType: 'WOW' });
const info = manager.getGameModeInfo();
expect(info.earlyEntry).toBeDefined();
expect(typeof info.earlyEntry.enabled).toBe('boolean');
expect(typeof info.earlyEntry.allowInFreeMode).toBe('boolean');
expect(typeof info.earlyEntry.allowInCreditsMode).toBe('boolean');
});
});
});