diff --git a/html/js/network/socketHandlers.js b/html/js/network/socketHandlers.js
index 86248c1..c7dc0be 100644
--- a/html/js/network/socketHandlers.js
+++ b/html/js/network/socketHandlers.js
@@ -1792,7 +1792,7 @@ const SocketHandlers = {
$('#ecX').on('click', function () { $('#entryChoiceOverlay').remove(); });
$('#ecFree').on('click', function () {
if (timing === 'now') begin('⚡ Free game — dropping in...', function () { socket.emit('auto_start', { free: true }); });
- else begin('🛡️ Free game — queued for the next block...', function () { socket.emit('join_queue'); });
+ else begin('🛡️ Free game — queued for the next block...', function () { socket.emit('join_queue', { free: true }); });
});
$('#ecRank').on('click', function () {
if (!hasCredits) { pay(); return; }
diff --git a/src/network/queueHandler.js b/src/network/queueHandler.js
index c106668..e9570e2 100644
--- a/src/network/queueHandler.js
+++ b/src/network/queueHandler.js
@@ -19,8 +19,13 @@ class QueueHandler {
/**
* Handle game queue request (typing "enter") with rate limiting
*/
- async handleGameQueue(socket, getUserBySocket) {
+ async handleGameQueue(socket, getUserBySocket, opts = {}) {
try {
+ // The player explicitly chose FREE play (Pleb board). Only honoured when the instance
+ // allows free play; otherwise fall through to the paid eligibility check below. Mirrors
+ // the auto_start free path so the "Next block · Free Play" choice doesn't wrongly pop the
+ // payment modal for a free queued game.
+ const wantsFree = opts.free === true && this.gameModeManager?.freePlayEnabled;
// Rate limiting for queue attempts
const rateLimitResult = await this.rateLimiter.checkLimit(socket.id, 'game:queue');
if (!rateLimitResult.allowed) {
@@ -52,22 +57,24 @@ class QueueHandler {
return;
}
- // Check payment eligibility and payout address requirements
- const paymentCheckResult = await this._checkPaymentEligibility(socket.id);
- if (!paymentCheckResult.allowed) {
- // Handle the specific reason for denial
- 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 join queue');
+ // Check payment eligibility and payout address requirements — SKIPPED for free play.
+ if (!wantsFree) {
+ const paymentCheckResult = await this._checkPaymentEligibility(socket.id);
+ if (!paymentCheckResult.allowed) {
+ // Handle the specific reason for denial
+ 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 join queue');
+ }
+ return;
}
- return;
}
// Record the queue attempt
@@ -82,13 +89,15 @@ class QueueHandler {
} catch (e) {}
}
- // Add to waiting queue (in free mode or already authorized paid mode)
+ // Add to waiting queue (in free mode or already authorized paid mode). `free` carries the
+ // Pleb-board intent to processGameStart when the block lands, so no credit/payment is taken.
this.queueManager.addPlayer({
serverId: socket.id,
clientId: currentUser.clientId,
userId: userId,
requiresConfirmation: false,
- confirmed: true
+ confirmed: true,
+ free: wantsFree
});
const currentBlock = this.debugManager.getCurrentBlockHeight();
diff --git a/src/network/queueManager.js b/src/network/queueManager.js
index a6c8eda..1ae7f28 100644
--- a/src/network/queueManager.js
+++ b/src/network/queueManager.js
@@ -22,9 +22,9 @@ class QueueManager {
this._waitingPlayers = []; // internal queue
}
- addPlayer({ serverId, clientId, userId = null, entryTime = Date.now(), paymentId = null, requiresConfirmation = false, confirmed = true }) {
+ addPlayer({ serverId, clientId, userId = null, entryTime = Date.now(), paymentId = null, requiresConfirmation = false, confirmed = true, free = false }) {
if (this.getPlayerIndex(serverId) !== -1) return; // already queued
- this._waitingPlayers.push({ serverId, clientId, userId, entryTime, paymentId, requiresConfirmation, confirmed });
+ this._waitingPlayers.push({ serverId, clientId, userId, entryTime, paymentId, requiresConfirmation, confirmed, free });
if (this.CONSOLE_LOGGING) {
console.log(`[QueueManager] Added player ${serverId} (userId=${userId}, confirmed=${confirmed}, requiresConfirmation=${requiresConfirmation}). Queue length: ${this._waitingPlayers.length}`);
}
@@ -117,9 +117,10 @@ class QueueManager {
gameState.proof = game.getProofCommitment();
}
- // Process game start (credits deduction / payment link)
+ // Process game start (credits deduction / payment link). forceFree honours a queued
+ // Pleb-board entry so no credit/payment is taken when the block lands.
if (this.gameModeManager) {
- const startRes = await this.gameModeManager.processGameStart(serverId, game.id);
+ const startRes = await this.gameModeManager.processGameStart(serverId, game.id, { forceFree: !!entry.free });
if (!startRes.success) {
// Abort game + clean up orphaned DB record
if (this.activeGames) this.activeGames.delete(serverId);
@@ -187,9 +188,10 @@ class QueueManager {
gameState.proof = game.getProofCommitment();
}
- // Process game start (credits deduction / payment link)
+ // Process game start (credits deduction / payment link). forceFree honours a queued
+ // Pleb-board entry confirmed after the tick.
if (this.gameModeManager) {
- const startRes = await this.gameModeManager.processGameStart(serverId, game.id);
+ const startRes = await this.gameModeManager.processGameStart(serverId, game.id, { forceFree: !!entry.free });
if (!startRes.success) {
// Abort game + clean up orphaned DB record
if (this.activeGames) this.activeGames.delete(serverId);
diff --git a/src/network/socketHandlers.js b/src/network/socketHandlers.js
index f0d56aa..b835be3 100644
--- a/src/network/socketHandlers.js
+++ b/src/network/socketHandlers.js
@@ -480,7 +480,7 @@ class SocketHandlers {
});
socket.on('auto_start', (data) => this.handleAutoStart(socket, (data && typeof data === 'object') ? data : {})); // New handler for start button
socket.on('play_free', (data) => this.handleAutoStart(socket, { ...((data && typeof data === 'object') ? data : {}), free: true })); // Explicit free-play choice
- socket.on('join_queue', () => this.handleJoinQueue(socket)); // Queue instead of auto-start
+ socket.on('join_queue', (data) => this.handleJoinQueue(socket, (data && typeof data === 'object') ? data : {})); // Queue instead of auto-start ({ free: true } for Pleb-board free play)
socket.on('early_entry', () => this.handleEarlyEntry(socket)); // Early entry without waiting for block
socket.on('address:prompt', () => this.handleAddressPrompt(socket));
@@ -662,11 +662,12 @@ class SocketHandlers {
/**
* Handle join_queue request — adds player to block queue instead of starting immediately
*/
- async handleJoinQueue(socket) {
+ async handleJoinQueue(socket, opts = {}) {
try {
await this.queueHandler.handleGameQueue(
socket,
- this.connectionHandler.getUserBySocket.bind(this.connectionHandler)
+ this.connectionHandler.getUserBySocket.bind(this.connectionHandler),
+ opts
);
// queueHandler already emits queue status and adds to queue
// Client receives queue_joined event from broadcastManager