mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-11-10 09:13:54 +01:00
feat: free-play choice + per-game dual leaderboard (backend)
Free-to-play alongside paid, with scores split per game between a Pleb board
(free) and the Hall of Champions (paid). Keys on the existing per-game
game_mode, so no new game columns.
- Leaderboard API: GET /api/leaderboard?board=pleb|champions|all. champions =
game_mode IN (PAID_SINGLE,PAID_CREDITS), pleb = FREE. board is whitelisted.
Migration 019 adds a supporting partial index.
- FREE_PLAY_ENABLED config: offer free play as an explicit choice even when the
instance also sells credits/entry (always on when payments are off).
- play_free socket event -> handleAutoStart(socket, {free:true}); skips all
payment/credits/payout-address gating and records the game as game_mode=FREE
via processGameStart(..., {forceFree:true}) -> _processGameStartFree (no
payment, no payout, counted).
- getPaymentOptionsForUser exposes a play_free option; /api/game-modes exposes
freePlayEnabled.
Adds freePlay.test.js. Full backend suite: 22 suites / 158 tests.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Free-play entry tests (Feature 1).
|
||||
*
|
||||
* When a player explicitly chooses free play (even on an instance that also sells
|
||||
* credits/entry), the game must be recorded as game_mode='FREE' (so it lands on the Pleb
|
||||
* leaderboard, not the Hall of Champions) with no payment and no payout.
|
||||
*/
|
||||
|
||||
const GameModeManager = require('../src/game/gameModeManager');
|
||||
|
||||
const createMockDb = () => {
|
||||
const client = { query: jest.fn().mockResolvedValue({ rows: [] }) };
|
||||
return {
|
||||
query: jest.fn().mockResolvedValue({ rows: [] }),
|
||||
withTransaction: jest.fn().mockImplementation(async (cb) => cb(client)),
|
||||
_client: client
|
||||
};
|
||||
};
|
||||
|
||||
const cfg = () => ({
|
||||
getConfig: () => ({
|
||||
paymentsEnabled: true,
|
||||
currency: { symbol: 'WOW', decimals: 11 },
|
||||
modes: {
|
||||
direct: { enabled: true, price: 100000000000n },
|
||||
credits: { enabled: true, creditsPerGame: 1, packages: [{ id: 'small', credits: 10, price: '500000000000', bonus: 0 }] }
|
||||
},
|
||||
payouts: { rules: { direct: { enabled: true, multipliers: { escape: 2, escapeWithTreasure: 3 } }, credits: { enabled: false, multipliers: { escape: 1.5, escapeWithTreasure: 2 }, baseValue: 50000000000n } } },
|
||||
preferences: { preferCreditsFirst: true },
|
||||
earlyEntry: { enabled: false }
|
||||
}),
|
||||
getLegacyGameMode: () => 'PAID_SINGLE',
|
||||
eventBus: { on: () => {} }
|
||||
});
|
||||
|
||||
function buildGmm() {
|
||||
const db = createMockDb();
|
||||
const gmm = new GameModeManager(db, { processPayout: jest.fn() }, { CONSOLE_LOGGING: false, getCurrentBlockHeight: () => 1 }, cfg());
|
||||
// Resolve identity straight to a stable user row.
|
||||
gmm.sessionManager = { getBySocket: jest.fn().mockResolvedValue({ id: 1 }) };
|
||||
db.query.mockImplementation(async (text) => {
|
||||
if (/SELECT \* FROM users WHERE id = \$1/i.test(text)) return { rows: [{ id: 1, credits: 0 }] };
|
||||
return { rows: [] };
|
||||
});
|
||||
return { gmm, db };
|
||||
}
|
||||
|
||||
describe('free-play game start (forceFree)', () => {
|
||||
test('records game_mode=FREE and counts the game, no payment', async () => {
|
||||
const { gmm, db } = buildGmm();
|
||||
|
||||
const res = await gmm.processGameStart('sock1', 'seed-123', { forceFree: true });
|
||||
|
||||
expect(res.success).toBe(true);
|
||||
expect(res.effectiveMode).toBe('FREE');
|
||||
const calls = db._client.query.mock.calls.map(c => c[0]);
|
||||
expect(calls.find(s => /UPDATE games SET game_mode = 'FREE'/i.test(s))).toBeDefined();
|
||||
expect(calls.find(s => /UPDATE users SET total_games_played/i.test(s))).toBeDefined();
|
||||
});
|
||||
|
||||
test('offers a play_free option when free play is enabled alongside payments', async () => {
|
||||
const { gmm } = buildGmm();
|
||||
gmm.freePlayEnabled = true;
|
||||
const opts = await gmm.getPaymentOptionsForUser('sock1');
|
||||
expect(opts.options.some(o => o.type === 'play_free' && o.mode === 'FREE')).toBe(true);
|
||||
});
|
||||
|
||||
test('does NOT offer free play when disabled', async () => {
|
||||
const { gmm } = buildGmm();
|
||||
gmm.freePlayEnabled = false;
|
||||
const opts = await gmm.getPaymentOptionsForUser('sock1');
|
||||
expect(opts.options.some(o => o.type === 'play_free')).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user