Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simulate generator function #5

Merged
merged 2 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,20 @@ module.exports = {
plugins: ['@typescript-eslint', 'jsdoc'],
overrides: [
{
files: ['*.js'],
files: ['*.js', '*.ts'],
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'after-used',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/no-var-requires': 'off',
},
},
Expand Down
49 changes: 15 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,60 +101,41 @@ const result = [

Given a list of hands and community cards, estimate how often each hand will win or tie using a [Monte Carlo simulation](https://en.wikipedia.org/wiki/Monte_Carlo_method) for roughly estimating the odds of a hand winning or tying.

The `simulate` [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/GeneratorFunction) returns a [generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) that can be used to run as many Monte Carlo simulations as desired, limited by the maximum number of simulations that are possible for a given scenario based upon the provided inputs. (For example, if there are only 2 streets remaining to be dealt with 45 cards remaining in the deck, then there are 45 \* 44 = 1,980 possible simulations.)

```ts
import { Hand, simulate } from '@poker-apprentice/hand-evaluator';

const hand1: Hand = ['As', 'Ks'];
const hand2: Hand = ['Jd', 'Jh'];

simulate({
const generate = simulate({
allHoleCards: [hand1, hand2],
communityCards: ['Qd', 'Js', '8d'],
expectedCommunityCardCount: 5,
expectedHoleCardCount: 2,
minimumHoleCardsUsed: 0,
maximumHoleCardsUsed: 2,
samples: 2000,
samplesPerUpdate: 500,
callback: (result) => {
const hand1WinPercent = ((result[0].wins / result[0].total) * 100).toFixed(1);
console.log(hand1WinPercent, result);
},
});

let result = generate.next();
while (!result.done) {
const hand1WinPercent = ((result[0].wins / result[0].total) * 100).toFixed(1);

// Output the cumulative results every 500 runs.
if (result[0].total % 500 === 0) {
console.log(hand1WinPercent, result);
}

result = generate.next();
}

// => "13.8" [{ wins: 69, ties: 0, total: 500 }, { wins: 431, ties: 0, total: 500 }]
// => "13.9" [{ wins: 139, ties: 0, total: 1000 }, { wins: 861, ties: 0, total: 1000 }]
// => "15.4" [{ wins: 231, ties: 0, total: 1500 }, { wins: 1269, ties: 0, total: 1500 }]
// => "14.8" [{ wins: 295, ties: 0, total: 2000 }, { wins: 1705, ties: 0, total: 2000 }]
```

Several options can be provided to specify how long to run the simulation, as well as how many samples should be generated per iteration. The greater the number of samples, the closer the result should be to the actual calculated value.

- `samples`: The total number of simulations to run.
- `samplesPerUpdate`: The number of simulations to run per iteration.

The `simulate` function returns another function that can be used abort/cancel the simulation.

```ts
const abort = simulate({
/* snip */
});
// later...
abort();
```

```ts
const abort = simulate({
// snip
callback: (result) => {
console.log(result);
if (result.total >= 10000) {
abort();
}
},
});
```

#### `odds`

Given a list of hands and community cards, determine how often each hand will win or tie.
Expand Down
11 changes: 4 additions & 7 deletions src/__tests__/helpers/simulateHoldem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import * as simulateModule from '../../simulate';
import { simulateHoldem } from '../../helpers/simulateHoldem';

describe('simulateHoldem', () => {
const callback = () => {};

it('delegates to `simulate`', () => {
const simulateSpy = jest.spyOn(simulateModule, 'simulate');

Expand All @@ -14,8 +12,8 @@ describe('simulateHoldem', () => {
];
const communityCards: Card[] = ['Ac', '9h', 'Qd', '2d', '2s'];

const abort = simulateHoldem({ allHoleCards, communityCards, callback });
abort();
const generate = simulateHoldem({ allHoleCards, communityCards });
generate.next();

expect(simulateSpy).toHaveBeenCalledWith({
allHoleCards,
Expand All @@ -24,7 +22,6 @@ describe('simulateHoldem', () => {
expectedHoleCardCount: 2,
minimumHoleCardsUsed: 0,
maximumHoleCardsUsed: 2,
callback,
});
});

Expand All @@ -35,7 +32,7 @@ describe('simulateHoldem', () => {
];
const communityCards: Card[] = ['Ac', '9h', 'Qd', '2d', '2s'];

expect(() => simulateHoldem({ allHoleCards, communityCards, callback })).toThrow(
expect(() => simulateHoldem({ allHoleCards, communityCards })).toThrow(
'Each collection of hole cards accept a maximum of 2 elements',
);
});
Expand All @@ -47,7 +44,7 @@ describe('simulateHoldem', () => {
];
const communityCards: Card[] = ['Ac', '9h', 'Qd', '2d', '2s', 'Td'];

expect(() => simulateHoldem({ allHoleCards, communityCards, callback })).toThrow(
expect(() => simulateHoldem({ allHoleCards, communityCards })).toThrow(
'communityCards accepts a maximum of 5 elements',
);
});
Expand Down
11 changes: 4 additions & 7 deletions src/__tests__/helpers/simulateOmaha.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import * as simulateModule from '../../simulate';
import { simulateOmaha } from '../../helpers/simulateOmaha';

describe('simulateOmaha', () => {
const callback = () => {};

it('delegates to `simulate`', () => {
const simulateSpy = jest.spyOn(simulateModule, 'simulate');

Expand All @@ -14,8 +12,8 @@ describe('simulateOmaha', () => {
];
const communityCards: Card[] = ['Ac', '9h', 'Qd', '2d', '2s'];

const abort = simulateOmaha({ allHoleCards, communityCards, callback });
abort();
const generate = simulateOmaha({ allHoleCards, communityCards });
generate.next();

expect(simulateSpy).toHaveBeenCalledWith({
allHoleCards,
Expand All @@ -24,7 +22,6 @@ describe('simulateOmaha', () => {
expectedHoleCardCount: 4,
minimumHoleCardsUsed: 2,
maximumHoleCardsUsed: 2,
callback,
});
});

Expand All @@ -35,7 +32,7 @@ describe('simulateOmaha', () => {
];
const communityCards: Card[] = ['Ac', '9h', 'Qd', '2d', '2s'];

expect(() => simulateOmaha({ allHoleCards, communityCards, callback })).toThrow(
expect(() => simulateOmaha({ allHoleCards, communityCards })).toThrow(
'Each collection of hole cards accept a maximum of 4 elements',
);
});
Expand All @@ -47,7 +44,7 @@ describe('simulateOmaha', () => {
];
const communityCards: Card[] = ['Ac', '9h', 'Qd', '2d', '2s', 'Td'];

expect(() => simulateOmaha({ allHoleCards, communityCards, callback })).toThrow(
expect(() => simulateOmaha({ allHoleCards, communityCards })).toThrow(
'communityCards accepts a maximum of 5 elements',
);
});
Expand Down
11 changes: 4 additions & 7 deletions src/__tests__/helpers/simulatePineapple.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import * as simulateModule from '../../simulate';
import { simulatePineapple } from '../../helpers/simulatePineapple';

describe('simulatePineapple', () => {
const callback = () => {};

it('delegates to `simulate`', () => {
const simulateSpy = jest.spyOn(simulateModule, 'simulate');

Expand All @@ -14,8 +12,8 @@ describe('simulatePineapple', () => {
];
const communityCards: Card[] = ['Ac', '9h', 'Qd', '2d', '2s'];

const abort = simulatePineapple({ allHoleCards, communityCards, callback });
abort();
const generate = simulatePineapple({ allHoleCards, communityCards });
generate.next();

expect(simulateSpy).toHaveBeenCalledWith({
allHoleCards,
Expand All @@ -24,7 +22,6 @@ describe('simulatePineapple', () => {
expectedHoleCardCount: 3,
minimumHoleCardsUsed: 0,
maximumHoleCardsUsed: 2,
callback,
});
});

Expand All @@ -35,7 +32,7 @@ describe('simulatePineapple', () => {
];
const communityCards: Card[] = ['Ac', '9h', 'Qd', '2d', '2s'];

expect(() => simulatePineapple({ allHoleCards, communityCards, callback })).toThrow(
expect(() => simulatePineapple({ allHoleCards, communityCards })).toThrow(
'Each collection of hole cards accept a maximum of 3 elements',
);
});
Expand All @@ -47,7 +44,7 @@ describe('simulatePineapple', () => {
];
const communityCards: Card[] = ['Ac', '9h', 'Qd', '2d', '2s', 'Td'];

expect(() => simulatePineapple({ allHoleCards, communityCards, callback })).toThrow(
expect(() => simulatePineapple({ allHoleCards, communityCards })).toThrow(
'communityCards accepts a maximum of 5 elements',
);
});
Expand Down
9 changes: 3 additions & 6 deletions src/__tests__/helpers/simulateStud.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import * as simulateModule from '../../simulate';
import { simulateStud } from '../../helpers/simulateStud';

describe('simulateStud', () => {
const callback = () => {};

it('delegates to `simulate`', () => {
const simulateSpy = jest.spyOn(simulateModule, 'simulate');

Expand All @@ -13,8 +11,8 @@ describe('simulateStud', () => {
['Jd', 'Jh', '2h', 'Jc', '2s', '3c', '4h'],
];

const abort = simulateStud({ allHoleCards, callback });
abort();
const generate = simulateStud({ allHoleCards });
generate.next();

expect(simulateSpy).toHaveBeenCalledWith({
allHoleCards,
Expand All @@ -23,7 +21,6 @@ describe('simulateStud', () => {
expectedHoleCardCount: 7,
minimumHoleCardsUsed: 0,
maximumHoleCardsUsed: 7,
callback,
});
});

Expand All @@ -33,7 +30,7 @@ describe('simulateStud', () => {
['Jd', 'Jh', '2h', 'Jc', '2s', '3c', '4h', '5d'],
];

expect(() => simulateStud({ allHoleCards, callback })).toThrow(
expect(() => simulateStud({ allHoleCards })).toThrow(
'Each collection of hole cards accept a maximum of 7 elements',
);
});
Expand Down
65 changes: 14 additions & 51 deletions src/__tests__/simulate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,58 +16,21 @@ describe('simulate', () => {
maximumHoleCardsUsed: 2,
};

it('calls callback function with results', (next) => {
simulate({
...options,
samples: 1000,
samplesPerUpdate: 1000,
callback: (odds) => {
expect(odds[0].total).toEqual(1000);
next();
},
});
it('yields results the correct number of times', () => {
const generate = simulate(options);
expect(generate.next().value[0].total).toEqual(1);
expect(generate.next().value[0].total).toEqual(2);
expect(generate.next().value[0].total).toEqual(3);
});

it('calculates the correct number of samples', (next) => {
const samples = 85;
const samplesPerUpdate = 10;
let callbackCount = 0;

simulate({
...options,
samples,
samplesPerUpdate,
callback: (odds) => {
callbackCount += 1;

const { total } = odds[0];

if (callbackCount === Math.ceil(samples / samplesPerUpdate)) {
expect(total).toEqual(samples);
next();
} else {
expect(total).toEqual(callbackCount * samplesPerUpdate);
}
},
});
});

it('aborts', (next) => {
let callbackCount = 0;
const maxIterations = 3;

const abort = simulate({
...options,
callback: () => {
callbackCount += 1;
if (callbackCount === maxIterations) {
abort();
setTimeout(() => {
expect(callbackCount).toEqual(maxIterations);
next();
}, 100);
}
},
});
it('returns after yielding the maximum number of possible times', () => {
let count = 0;
for (const _result of simulate(options)) {
count += 1;
}
const remainingCardCount =
52 - communityCards.length - allHoleCards.map((c) => c.length).reduce((a, c) => a + c, 0);
const permutationCount = remainingCardCount * (remainingCardCount - 1);
expect(count).toEqual(permutationCount);
});
});
Loading
Loading