Files
jwinterm fad2a695bd Wownerogue
A browser-based roguelike synchronized with crypto block timing. A run is a race
against the next block: reach the exit before the block lands, or the house wins.
The same codebase runs the solo dungeon, the social Tavern, and operator-selected
PvP and race rulesets, with separate free and paid leaderboards.

Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md
for the design, and docs/DEPLOY.md to operate an instance.
2026-07-27 08:23:01 -04:00

75 lines
3.0 KiB
JavaScript

/**
* 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);
});
});