try to fix this spaghetti of a code
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const engine = require('../lib/engine');
|
||||
const config = {ante: 10, now: () => 100, turnMs: 30000, intermissionMs: 30000};
|
||||
function game(stacks = {a: 200, b: 200, c: 200}) {return engine.deal(Object.keys(stacks), stacks, config);}
|
||||
function act(g, kind, amount) {engine.action(g, engine.turn(g), kind, amount, config);}
|
||||
test('complete checked hand conserves all chips and reveals exactly five board cards', () => {
|
||||
const g = game();
|
||||
for (let i = 0; i < 20 && g.phase === 'betting'; i++) act(g, 'hit');
|
||||
assert.equal(g.phase, 'intermission'); assert.equal(g.communityCards.length, 5);
|
||||
assert.equal(Object.values(g.stacks).reduce((a,b) => a+b,0), 600); assert.equal(g.pot, 0);
|
||||
});
|
||||
test('short all-in does not reopen raising for an earlier actor', () => {
|
||||
const g = game({a: 200, b: 200, c: 25});
|
||||
assert.equal(engine.turn(g), 'b'); act(g, 'bet', 10);
|
||||
assert.equal(engine.turn(g), 'c'); act(g, 'bet', 15);
|
||||
assert.equal(engine.turn(g), 'a'); act(g, 'hit');
|
||||
assert.equal(engine.turn(g), 'b');
|
||||
assert.throws(() => act(g, 'bet', 20), error => error.code === 'RAISE_NOT_REOPENED');
|
||||
act(g, 'hit'); assert.equal(g.street, 'flop');
|
||||
});
|
||||
test('minimum raise and whole-chip validation do not change state on failure', () => {
|
||||
const g = game(); act(g, 'bet', 20);
|
||||
const before = JSON.stringify(g);
|
||||
for (const amount of [21, 0.5, '20', -1, Infinity]) assert.throws(() => act(g, 'bet', amount));
|
||||
assert.equal(JSON.stringify(g), before);
|
||||
});
|
||||
test('timeout folds facing a bet and checks for free', () => {
|
||||
const g = game(); act(g, 'bet', 20);
|
||||
const player = engine.turn(g), stack = g.stacks[player];
|
||||
act(g, 'timeout'); assert.equal(g.stacks[player], stack); assert.equal(g.folded[player], true);
|
||||
const free = game(); act(free, 'timeout'); assert.equal(free.folded.b, false); assert.equal(free.stacks.b, 190);
|
||||
});
|
||||
test('ante-only all-ins run out immediately and settlement pays exactly the pot', () => {
|
||||
const g = game({a: 10, b: 10}); assert.equal(g.phase, 'intermission');
|
||||
assert.equal(g.communityCards.length, 5); assert.equal(g.stacks.a + g.stacks.b, 20);
|
||||
});
|
||||
test('leaving preserves seat order and refunds unclaimable contribution layers', () => {
|
||||
const g = game(); const current = engine.turn(g); engine.leave(g, 'a', config);
|
||||
assert.equal(engine.turn(g), current); engine.leave(g, 'b', config);
|
||||
assert.equal(g.phase, 'intermission'); assert.equal(Object.values(g.stacks).reduce((a,b) => a+b,0), 600);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const {createRepository} = require('../lib/repository');
|
||||
const {migrateLegacy} = require('../scripts/migrate-legacy');
|
||||
test('legacy import validates all rows, backs up source, and is idempotent', async t => {
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(),'gambling-migration-'));
|
||||
t.after(()=>fs.rm(directory,{recursive:true,force:true}));
|
||||
const file = path.join(directory,'data.json');
|
||||
const repo = await createRepository(':memory:'); t.after(()=>repo.close());
|
||||
const source = JSON.stringify([{username:' Alice ',password:'password',money:123},{username:'bob',password:'password',money:200}]);
|
||||
await fs.writeFile(file,source);
|
||||
const result = await migrateLegacy(repo,file,4);
|
||||
assert.equal(result.imported,2); assert.equal(await fs.readFile(result.backup,'utf8'),source);
|
||||
assert.equal(await fs.readFile(file,'utf8'),source);
|
||||
assert.equal((await migrateLegacy(repo,file,4)).alreadyApplied,true);
|
||||
assert.equal((await repo.user('alice')).money,123);
|
||||
await fs.writeFile(file,JSON.stringify([{username:'valid',password:'password'},{username:'invalid',password:'password',money:-1}]));
|
||||
await assert.rejects(migrateLegacy(repo,file,4),/Invalid legacy balance/);
|
||||
assert.equal(await repo.user('valid'),undefined);
|
||||
});
|
||||
test('migration database failure rolls back every row and records no applied marker', async t => {
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(),'gambling-migration-'));
|
||||
t.after(()=>fs.rm(directory,{recursive:true,force:true}));
|
||||
const file=path.join(directory,'data.json');
|
||||
const repo=await createRepository(':memory:'); t.after(()=>repo.close());
|
||||
await repo.register('existing','unused',null);
|
||||
await fs.writeFile(file,JSON.stringify([{username:'new-user',password:'password'},{username:'existing',password:'password'}]));
|
||||
await assert.rejects(migrateLegacy(repo,file,4),/already exists/);
|
||||
assert.equal(await repo.user('new-user'),undefined);
|
||||
await assert.rejects(repo.transaction(tx=>tx.run('UPDATE users SET money = -1 WHERE username = ?', 'existing')));
|
||||
assert.equal((await repo.user('existing')).money,200);
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const {createApplication} = require('../gambling');
|
||||
const {createRepository} = require('../lib/repository');
|
||||
const {createLobbyService} = require('../lib/lobbies');
|
||||
const secret = 'test-secret-with-at-least-thirty-two-characters';
|
||||
const command = (service, username, type, fields = {}) => service.command(username, {type, requestId: crypto.randomUUID(), ...fields});
|
||||
async function setup(t, overrides = {}) {
|
||||
let now = 100000;
|
||||
const api = await createApplication({secret, databasePath: ':memory:', bcryptRounds: 4, now: () => now, intermissionMs: 60000, ...overrides});
|
||||
t.after(() => api.close());
|
||||
for (const username of ['a','b','c']) await api.sessions.register({username, password: 'password'});
|
||||
if (overrides.startingBalances) await api.repository.transaction(async tx => {
|
||||
for (const [username, money] of Object.entries(overrides.startingBalances)) await tx.run('UPDATE users SET money = ? WHERE username = ?', money, username);
|
||||
});
|
||||
const {lobbyId} = await command(api.lobbies, 'a', 'lobby:create');
|
||||
for (const username of ['b','c']) await command(api.lobbies, username, 'lobby:join', {lobbyId});
|
||||
await command(api.lobbies, 'a', 'poker:start', {lobbyId});
|
||||
const intermission = api.lobbies.get(lobbyId).game; now = intermission.nextRoundStartsAt;
|
||||
await command(api.lobbies, null, 'timer', {lobbyId, handId: intermission.id, phase: 'intermission', deadline: now});
|
||||
return {api, lobbyId};
|
||||
}
|
||||
test('accepted actions persist once; failed commits leave both memory and database unchanged', async t => {
|
||||
let fail = false;
|
||||
const {api, lobbyId} = await setup(t, {repositoryHooks: {beforeCommit: async () => {if (fail) {fail = false; throw new Error('Injected disk failure');}}}});
|
||||
const message = {type: 'poker:action', lobbyId, requestId: crypto.randomUUID(), action: 'bet', payload: {amount: 20}};
|
||||
const before = JSON.stringify(api.lobbies.get(lobbyId)); fail = true;
|
||||
await assert.rejects(api.lobbies.command('b', message), /Injected/);
|
||||
assert.equal(JSON.stringify(api.lobbies.get(lobbyId)), before);
|
||||
assert.equal((await api.repository.user('b')).money, 190);
|
||||
await Promise.all([api.lobbies.command('b', message), api.lobbies.command('b', message)]);
|
||||
assert.equal((await api.repository.user('b')).money, 170);
|
||||
assert.equal(api.lobbies.get(lobbyId).game.pot, 50);
|
||||
await assert.rejects(api.lobbies.command('b', {...message, action: 'fold'}), error => error.code === 'IDEMPOTENCY_CONFLICT');
|
||||
assert.deepEqual(await api.repository.totals(), {users: 550, escrow: 50});
|
||||
});
|
||||
test('concurrent account joins and table starts serialize; simultaneous leaves conserve chips', async t => {
|
||||
const {api, lobbyId} = await setup(t);
|
||||
const results = await Promise.allSettled([command(api.lobbies, 'a', 'lobby:create'), command(api.lobbies, 'a', 'poker:start', {lobbyId})]);
|
||||
assert.ok(results.every(result => result.status === 'rejected'));
|
||||
await Promise.all(['a', 'b', 'c'].map(username => command(api.lobbies, username, 'lobby:leave', {lobbyId})));
|
||||
assert.deepEqual(await api.repository.totals(), {users: 600, escrow: 0}); assert.equal(api.lobbies.get(lobbyId), undefined);
|
||||
});
|
||||
test('stale timer cannot act on a new turn', async t => {
|
||||
const {api, lobbyId} = await setup(t);
|
||||
const original = api.lobbies.get(lobbyId);
|
||||
const game = original.game;
|
||||
await command(api.lobbies, 'b', 'poker:action', {lobbyId, action: 'hit'});
|
||||
const before = JSON.stringify(api.lobbies.get(lobbyId));
|
||||
await command(api.lobbies, null, 'timer', {lobbyId, handId: game.id, phase: 'betting', deadline: game.turnEndsAt, revision: original.revision});
|
||||
assert.equal(JSON.stringify(api.lobbies.get(lobbyId)), before);
|
||||
});
|
||||
test('unequal all-ins settle correct side pots atomically even after a settlement write fails', async t => {
|
||||
let fail = false;
|
||||
const draws = 'AS AH KS KH QS QH 2D 3C 7S 8H 9D'.split(' ').map(value => ({rank:value.slice(0,-1),suit:value.slice(-1)}));
|
||||
const {api, lobbyId} = await setup(t, {startingBalances:{a:60,b:200,c:100}, createDeck:()=>[
|
||||
...require('../lib/poker').createDeck().filter(card=>!draws.some(d=>d.rank===card.rank && d.suit===card.suit)), ...draws.toReversed()
|
||||
], repositoryHooks:{beforeCommit:async()=>{if(fail){fail=false;throw new Error('Settlement disk failure');}}}});
|
||||
await command(api.lobbies,'b','poker:action',{lobbyId,action:'bet',payload:{amount:190}});
|
||||
await command(api.lobbies,'c','poker:action',{lobbyId,action:'hit'});
|
||||
const message={type:'poker:action',lobbyId,requestId:crypto.randomUUID(),action:'hit'};
|
||||
fail=true;
|
||||
await assert.rejects(api.lobbies.command('a',message),/Settlement disk failure/);
|
||||
assert.equal(api.lobbies.get(lobbyId).game.phase,'betting');
|
||||
assert.deepEqual(await api.repository.totals(),{users:50,escrow:310});
|
||||
await api.lobbies.command('a',message);
|
||||
await api.lobbies.command('a',message);
|
||||
const game=api.lobbies.get(lobbyId).game;
|
||||
assert.equal(game.phase,'intermission');
|
||||
assert.deepEqual({...game.payouts},{a:180,b:180});
|
||||
for(const username of ['a','b','c']) assert.equal((await api.repository.user(username)).money,game.stacks[username]);
|
||||
assert.deepEqual(await api.repository.totals(),{users:360,escrow:0});
|
||||
});
|
||||
test('reopening a database refunds interrupted escrow exactly once', async t => {
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'gambling-recovery-'));
|
||||
t.after(() => fs.rm(directory, {recursive: true, force: true}));
|
||||
const filename = path.join(directory, 'test.sqlite');
|
||||
const repo = await createRepository(filename);
|
||||
for (const username of ['a','b']) await repo.register(username, 'unused-test-hash', null);
|
||||
let now = 1;
|
||||
const service = createLobbyService(repo, {ante: 10, intermissionMs: 60000, turnMs: 30000, now: () => now});
|
||||
const {lobbyId} = await command(service, 'a', 'lobby:create');
|
||||
await command(service, 'b', 'lobby:join', {lobbyId}); await command(service, 'a', 'poker:start', {lobbyId});
|
||||
const game = service.get(lobbyId).game; now = game.nextRoundStartsAt;
|
||||
await command(service, null, 'timer', {lobbyId, handId: game.id, phase: 'intermission', deadline: now});
|
||||
await command(service, 'b', 'poker:action', {lobbyId, action: 'bet', payload: {amount: 30}});
|
||||
assert.deepEqual(await repo.totals(), {users: 350, escrow: 50});
|
||||
await service.close(); await repo.close();
|
||||
const restarted = await createRepository(filename);
|
||||
try {
|
||||
await restarted.recover(); await restarted.recover();
|
||||
assert.deepEqual(await restarted.totals(), {users: 400, escrow: 0});
|
||||
assert.equal((await restarted.user('a')).money, 200); assert.equal((await restarted.user('b')).money, 200);
|
||||
} finally {await restarted.close();}
|
||||
});
|
||||
Reference in New Issue
Block a user