@darksol/terminal 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli.js +221 -24
- package/src/llm/engine.js +8 -9
- package/src/llm/models.js +67 -0
- package/src/services/poker.js +937 -0
- package/src/setup/wizard.js +61 -3
- package/src/web/commands.js +351 -5
- package/src/web/server.js +6 -4
- package/src/web/terminal.js +1 -1
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { fetchJSON } from '../utils/fetch.js';
|
|
3
|
+
import { getServiceURL, getConfig } from '../config/store.js';
|
|
4
|
+
import { isSignerRunning, fetchWithX402 } from '../utils/x402.js';
|
|
5
|
+
|
|
6
|
+
const getURL = () => getServiceURL('casino') || 'https://casino.darksol.net';
|
|
7
|
+
|
|
8
|
+
const SUITS = ['s', 'h', 'd', 'c'];
|
|
9
|
+
const SUIT_NAMES = { s: 'Spades', h: 'Hearts', d: 'Diamonds', c: 'Clubs' };
|
|
10
|
+
const RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A'];
|
|
11
|
+
const RANK_TO_VALUE = {
|
|
12
|
+
'2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
|
|
13
|
+
T: 10, J: 11, Q: 12, K: 13, A: 14,
|
|
14
|
+
};
|
|
15
|
+
const VALUE_TO_RANK = {
|
|
16
|
+
2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9',
|
|
17
|
+
10: 'T', 11: 'J', 12: 'Q', 13: 'K', 14: 'A',
|
|
18
|
+
};
|
|
19
|
+
const HAND_LABELS = [
|
|
20
|
+
'High Card',
|
|
21
|
+
'One Pair',
|
|
22
|
+
'Two Pair',
|
|
23
|
+
'Three of a Kind',
|
|
24
|
+
'Straight',
|
|
25
|
+
'Flush',
|
|
26
|
+
'Full House',
|
|
27
|
+
'Four of a Kind',
|
|
28
|
+
'Straight Flush',
|
|
29
|
+
'Royal Flush',
|
|
30
|
+
];
|
|
31
|
+
const STARTING_STACK = 100;
|
|
32
|
+
const SMALL_BLIND = 1;
|
|
33
|
+
const BIG_BLIND = 2;
|
|
34
|
+
const DEFAULT_BUY_IN_USDC = 1;
|
|
35
|
+
const DEFAULT_PAYOUT_USDC = 2;
|
|
36
|
+
const MAX_HISTORY = 20;
|
|
37
|
+
|
|
38
|
+
const games = new Map();
|
|
39
|
+
const history = [];
|
|
40
|
+
let dealerToggle = 'player';
|
|
41
|
+
|
|
42
|
+
function clone(value) {
|
|
43
|
+
return JSON.parse(JSON.stringify(value));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createDeck() {
|
|
47
|
+
const deck = [];
|
|
48
|
+
for (const suit of SUITS) {
|
|
49
|
+
for (const rank of RANKS) {
|
|
50
|
+
deck.push(`${rank}${suit}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return deck;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function shuffleDeck(deck, random = Math.random) {
|
|
57
|
+
const out = deck.slice();
|
|
58
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
59
|
+
const j = Math.floor(random() * (i + 1));
|
|
60
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseCard(card) {
|
|
66
|
+
const rank = card[0];
|
|
67
|
+
const suit = card[1];
|
|
68
|
+
return { card, rank, suit, value: RANK_TO_VALUE[rank] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cardList(cards) {
|
|
72
|
+
return cards.map(parseCard);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function compareArrays(a, b) {
|
|
76
|
+
const len = Math.max(a.length, b.length);
|
|
77
|
+
for (let i = 0; i < len; i++) {
|
|
78
|
+
const diff = (a[i] || 0) - (b[i] || 0);
|
|
79
|
+
if (diff !== 0) return diff;
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function combinations(items, size) {
|
|
85
|
+
const result = [];
|
|
86
|
+
const combo = [];
|
|
87
|
+
function walk(start, remaining) {
|
|
88
|
+
if (remaining === 0) {
|
|
89
|
+
result.push(combo.slice());
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (let i = start; i <= items.length - remaining; i++) {
|
|
93
|
+
combo.push(items[i]);
|
|
94
|
+
walk(i + 1, remaining - 1);
|
|
95
|
+
combo.pop();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
walk(0, size);
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function pluralRank(value) {
|
|
103
|
+
const name = VALUE_TO_RANK[value];
|
|
104
|
+
if (name === '6') return 'Sixes';
|
|
105
|
+
if (name === '5') return 'Fives';
|
|
106
|
+
if (name === '4') return 'Fours';
|
|
107
|
+
if (name === '3') return 'Threes';
|
|
108
|
+
if (name === '2') return 'Twos';
|
|
109
|
+
if (name === 'T') return 'Tens';
|
|
110
|
+
if (name === 'J') return 'Jacks';
|
|
111
|
+
if (name === 'Q') return 'Queens';
|
|
112
|
+
if (name === 'K') return 'Kings';
|
|
113
|
+
if (name === 'A') return 'Aces';
|
|
114
|
+
return `${name}s`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function singularRank(value) {
|
|
118
|
+
const name = VALUE_TO_RANK[value];
|
|
119
|
+
if (name === 'T') return 'Ten';
|
|
120
|
+
if (name === 'J') return 'Jack';
|
|
121
|
+
if (name === 'Q') return 'Queen';
|
|
122
|
+
if (name === 'K') return 'King';
|
|
123
|
+
if (name === 'A') return 'Ace';
|
|
124
|
+
return name;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function straightHigh(values) {
|
|
128
|
+
const uniq = [...new Set(values)].sort((a, b) => b - a);
|
|
129
|
+
if (uniq.includes(14)) uniq.push(1);
|
|
130
|
+
for (let i = 0; i <= uniq.length - 5; i++) {
|
|
131
|
+
let ok = true;
|
|
132
|
+
for (let j = 1; j < 5; j++) {
|
|
133
|
+
if (uniq[i + j] !== uniq[i] - j) {
|
|
134
|
+
ok = false;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (ok) return uniq[i] === 1 ? 5 : uniq[i];
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function evaluateFiveCards(cards) {
|
|
144
|
+
const parsed = cardList(cards).sort((a, b) => b.value - a.value);
|
|
145
|
+
const values = parsed.map(c => c.value);
|
|
146
|
+
const suits = parsed.map(c => c.suit);
|
|
147
|
+
const counts = new Map();
|
|
148
|
+
for (const value of values) counts.set(value, (counts.get(value) || 0) + 1);
|
|
149
|
+
const groups = [...counts.entries()].sort((a, b) => {
|
|
150
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
151
|
+
return b[0] - a[0];
|
|
152
|
+
});
|
|
153
|
+
const isFlush = suits.every(s => s === suits[0]);
|
|
154
|
+
const straight = straightHigh(values);
|
|
155
|
+
const topValues = values.slice().sort((a, b) => b - a);
|
|
156
|
+
|
|
157
|
+
if (isFlush && straight === 14) {
|
|
158
|
+
return {
|
|
159
|
+
category: 9,
|
|
160
|
+
label: HAND_LABELS[9],
|
|
161
|
+
name: 'Royal Flush',
|
|
162
|
+
tiebreak: [14],
|
|
163
|
+
cards,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (isFlush && straight) {
|
|
168
|
+
return {
|
|
169
|
+
category: 8,
|
|
170
|
+
label: HAND_LABELS[8],
|
|
171
|
+
name: `Straight Flush, ${singularRank(straight)} high`,
|
|
172
|
+
tiebreak: [straight],
|
|
173
|
+
cards,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (groups[0][1] === 4) {
|
|
178
|
+
return {
|
|
179
|
+
category: 7,
|
|
180
|
+
label: HAND_LABELS[7],
|
|
181
|
+
name: `Four of a Kind, ${pluralRank(groups[0][0])}`,
|
|
182
|
+
tiebreak: [groups[0][0], groups[1][0]],
|
|
183
|
+
cards,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (groups[0][1] === 3 && groups[1][1] === 2) {
|
|
188
|
+
return {
|
|
189
|
+
category: 6,
|
|
190
|
+
label: HAND_LABELS[6],
|
|
191
|
+
name: `Full House, ${pluralRank(groups[0][0])} over ${pluralRank(groups[1][0])}`,
|
|
192
|
+
tiebreak: [groups[0][0], groups[1][0]],
|
|
193
|
+
cards,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (isFlush) {
|
|
198
|
+
return {
|
|
199
|
+
category: 5,
|
|
200
|
+
label: HAND_LABELS[5],
|
|
201
|
+
name: `${SUIT_NAMES[suits[0]]} Flush, ${singularRank(topValues[0])} high`,
|
|
202
|
+
tiebreak: topValues,
|
|
203
|
+
cards,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (straight) {
|
|
208
|
+
return {
|
|
209
|
+
category: 4,
|
|
210
|
+
label: HAND_LABELS[4],
|
|
211
|
+
name: `Straight, ${singularRank(straight)} high`,
|
|
212
|
+
tiebreak: [straight],
|
|
213
|
+
cards,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (groups[0][1] === 3) {
|
|
218
|
+
const kickers = groups.slice(1).map(([value]) => value).sort((a, b) => b - a);
|
|
219
|
+
return {
|
|
220
|
+
category: 3,
|
|
221
|
+
label: HAND_LABELS[3],
|
|
222
|
+
name: `Three of a Kind, ${pluralRank(groups[0][0])}`,
|
|
223
|
+
tiebreak: [groups[0][0], ...kickers],
|
|
224
|
+
cards,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (groups[0][1] === 2 && groups[1][1] === 2) {
|
|
229
|
+
const pairValues = groups.filter(([, count]) => count === 2).map(([value]) => value).sort((a, b) => b - a);
|
|
230
|
+
const kicker = groups.find(([, count]) => count === 1)[0];
|
|
231
|
+
return {
|
|
232
|
+
category: 2,
|
|
233
|
+
label: HAND_LABELS[2],
|
|
234
|
+
name: `Two Pair, ${pluralRank(pairValues[0])} and ${pluralRank(pairValues[1])}`,
|
|
235
|
+
tiebreak: [...pairValues, kicker],
|
|
236
|
+
cards,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (groups[0][1] === 2) {
|
|
241
|
+
const kickers = groups.slice(1).map(([value]) => value).sort((a, b) => b - a);
|
|
242
|
+
return {
|
|
243
|
+
category: 1,
|
|
244
|
+
label: HAND_LABELS[1],
|
|
245
|
+
name: `One Pair, ${pluralRank(groups[0][0])}`,
|
|
246
|
+
tiebreak: [groups[0][0], ...kickers],
|
|
247
|
+
cards,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
category: 0,
|
|
253
|
+
label: HAND_LABELS[0],
|
|
254
|
+
name: `High Card, ${singularRank(topValues[0])}`,
|
|
255
|
+
tiebreak: topValues,
|
|
256
|
+
cards,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function evaluateHand(cards) {
|
|
261
|
+
if (cards.length < 5) {
|
|
262
|
+
throw new Error('Need at least five cards to evaluate a hand');
|
|
263
|
+
}
|
|
264
|
+
let best = null;
|
|
265
|
+
for (const combo of combinations(cards, 5)) {
|
|
266
|
+
const eval5 = evaluateFiveCards(combo);
|
|
267
|
+
if (!best) {
|
|
268
|
+
best = eval5;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (eval5.category > best.category || (eval5.category === best.category && compareArrays(eval5.tiebreak, best.tiebreak) > 0)) {
|
|
272
|
+
best = eval5;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return best;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function compareHands(a, b) {
|
|
279
|
+
if (a.category !== b.category) return a.category - b.category;
|
|
280
|
+
return compareArrays(a.tiebreak, b.tiebreak);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function activePlayer(game, actor) {
|
|
284
|
+
return actor === 'player' ? game.player : game.house;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function opponent(game, actor) {
|
|
288
|
+
return actor === 'player' ? game.house : game.player;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function nextStreet(street) {
|
|
292
|
+
if (street === 'preflop') return 'flop';
|
|
293
|
+
if (street === 'flop') return 'turn';
|
|
294
|
+
if (street === 'turn') return 'river';
|
|
295
|
+
if (street === 'river') return 'showdown';
|
|
296
|
+
return 'finished';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function postBlind(game, actor, amount) {
|
|
300
|
+
const seat = activePlayer(game, actor);
|
|
301
|
+
const paid = Math.min(seat.stack, amount);
|
|
302
|
+
seat.stack -= paid;
|
|
303
|
+
seat.bet += paid;
|
|
304
|
+
seat.committed += paid;
|
|
305
|
+
if (seat.stack === 0) seat.allIn = true;
|
|
306
|
+
game.pot += paid;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function dealOne(game) {
|
|
310
|
+
const card = game.deck.shift();
|
|
311
|
+
if (!card) throw new Error('Deck exhausted');
|
|
312
|
+
return card;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function dealHoleCards(game) {
|
|
316
|
+
const order = game.dealer === 'player' ? ['player', 'house', 'player', 'house'] : ['house', 'player', 'house', 'player'];
|
|
317
|
+
for (const actor of order) {
|
|
318
|
+
activePlayer(game, actor).hole.push(dealOne(game));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function burn(game) {
|
|
323
|
+
game.deck.shift();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function dealBoard(game, count) {
|
|
327
|
+
burn(game);
|
|
328
|
+
for (let i = 0; i < count; i++) {
|
|
329
|
+
game.community.push(dealOne(game));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getToCall(game, actor) {
|
|
334
|
+
const seat = activePlayer(game, actor);
|
|
335
|
+
const opp = opponent(game, actor);
|
|
336
|
+
return Math.max(0, opp.bet - seat.bet);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function fixedBetSize(game) {
|
|
340
|
+
return game.street === 'turn' || game.street === 'river' ? BIG_BLIND * 2 : BIG_BLIND;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function plannedBetSize(game, actor, intent = 'bet') {
|
|
344
|
+
const seat = activePlayer(game, actor);
|
|
345
|
+
const toCall = getToCall(game, actor);
|
|
346
|
+
const base = fixedBetSize(game);
|
|
347
|
+
const potHalf = Math.max(base, Math.round(game.pot / 2));
|
|
348
|
+
const target = intent === 'raise'
|
|
349
|
+
? Math.max(base * 2, toCall + potHalf)
|
|
350
|
+
: potHalf;
|
|
351
|
+
return Math.max(base, Math.min(target, seat.stack));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function getAvailableActions(game, actor = game.currentActor) {
|
|
355
|
+
if (!actor || !['player', 'house'].includes(actor)) return [];
|
|
356
|
+
if (game.street === 'finished' || game.street === 'showdown') return [];
|
|
357
|
+
const seat = activePlayer(game, actor);
|
|
358
|
+
const toCall = getToCall(game, actor);
|
|
359
|
+
const actions = [];
|
|
360
|
+
|
|
361
|
+
if (toCall > 0) {
|
|
362
|
+
actions.push('fold');
|
|
363
|
+
actions.push('call');
|
|
364
|
+
if (!seat.allIn && !opponent(game, actor).allIn && seat.stack > toCall && game.raisesThisStreet < 2) {
|
|
365
|
+
actions.push('raise');
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
actions.push('check');
|
|
369
|
+
if (!seat.allIn && !opponent(game, actor).allIn && seat.stack > 0) {
|
|
370
|
+
actions.push('bet');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (!seat.allIn && seat.stack > 0) {
|
|
375
|
+
actions.push('all-in');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return [...new Set(actions)];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function rankStrength(category) {
|
|
382
|
+
return category / 9;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function holeStrength(hole) {
|
|
386
|
+
const [a, b] = cardList(hole).sort((x, y) => y.value - x.value);
|
|
387
|
+
const pair = a.value === b.value;
|
|
388
|
+
const suited = a.suit === b.suit;
|
|
389
|
+
const gap = Math.abs(a.value - b.value);
|
|
390
|
+
let score = 0;
|
|
391
|
+
|
|
392
|
+
if (pair) score += 0.45 + (a.value / 14) * 0.4;
|
|
393
|
+
else score += (a.value + b.value) / 40;
|
|
394
|
+
|
|
395
|
+
if (suited) score += 0.08;
|
|
396
|
+
if (gap === 1) score += 0.08;
|
|
397
|
+
if (gap === 2) score += 0.04;
|
|
398
|
+
if (a.value >= 13) score += 0.08;
|
|
399
|
+
if (b.value >= 11) score += 0.05;
|
|
400
|
+
if (a.value >= 10 && b.value >= 10) score += 0.08;
|
|
401
|
+
|
|
402
|
+
return Math.min(score, 1);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function getDrawProfile(cards) {
|
|
406
|
+
const parsed = cardList(cards);
|
|
407
|
+
const suitCounts = parsed.reduce((acc, card) => {
|
|
408
|
+
acc[card.suit] = (acc[card.suit] || 0) + 1;
|
|
409
|
+
return acc;
|
|
410
|
+
}, {});
|
|
411
|
+
const flushDraw = Object.values(suitCounts).some(count => count === 4);
|
|
412
|
+
|
|
413
|
+
const values = [...new Set(parsed.map(card => card.value))].sort((a, b) => a - b);
|
|
414
|
+
if (values.includes(14)) values.unshift(1);
|
|
415
|
+
let maxRun = 1;
|
|
416
|
+
let run = 1;
|
|
417
|
+
for (let i = 1; i < values.length; i++) {
|
|
418
|
+
if (values[i] === values[i - 1] + 1) {
|
|
419
|
+
run++;
|
|
420
|
+
maxRun = Math.max(maxRun, run);
|
|
421
|
+
} else if (values[i] !== values[i - 1]) {
|
|
422
|
+
run = 1;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const openEnded = maxRun >= 4;
|
|
426
|
+
const gutshot = !openEnded && maxRun >= 3;
|
|
427
|
+
return { flushDraw, openEnded, gutshot };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function postflopStrength(hole, community) {
|
|
431
|
+
const cards = [...hole, ...community];
|
|
432
|
+
const made = evaluateHand(cards);
|
|
433
|
+
const draw = getDrawProfile(cards);
|
|
434
|
+
const holeVals = cardList(hole).map(card => card.value).sort((a, b) => b - a);
|
|
435
|
+
const boardVals = cardList(community).map(card => card.value).sort((a, b) => b - a);
|
|
436
|
+
const topPair = made.category === 1 && holeVals.some(value => value === boardVals[0]);
|
|
437
|
+
|
|
438
|
+
let equity = rankStrength(made.category);
|
|
439
|
+
if (topPair) equity += 0.12;
|
|
440
|
+
if (draw.flushDraw) equity += 0.16;
|
|
441
|
+
if (draw.openEnded) equity += 0.13;
|
|
442
|
+
if (draw.gutshot) equity += 0.06;
|
|
443
|
+
if (holeVals[0] >= 13 && holeVals[1] >= 10) equity += 0.04;
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
made,
|
|
447
|
+
draw,
|
|
448
|
+
equity: Math.min(equity, 1),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function decideHouseAction(game) {
|
|
453
|
+
const actions = getAvailableActions(game, 'house');
|
|
454
|
+
const toCall = getToCall(game, 'house');
|
|
455
|
+
const potOdds = toCall > 0 ? toCall / (game.pot + toCall) : 0;
|
|
456
|
+
const position = game.dealer === 'house'
|
|
457
|
+
? (game.street === 'preflop' ? 'button' : 'in_position')
|
|
458
|
+
: (game.street === 'preflop' ? 'big_blind' : 'out_of_position');
|
|
459
|
+
|
|
460
|
+
if (game.street === 'preflop') {
|
|
461
|
+
const score = holeStrength(game.house.hole);
|
|
462
|
+
const openThreshold = position === 'button' ? 0.42 : 0.58;
|
|
463
|
+
const raiseThreshold = position === 'button' ? 0.68 : 0.74;
|
|
464
|
+
const defendThreshold = Math.min(0.7, potOdds + (position === 'big_blind' ? 0.14 : 0.2));
|
|
465
|
+
|
|
466
|
+
if (toCall === 0) {
|
|
467
|
+
if (actions.includes('bet') && score >= openThreshold) return 'bet';
|
|
468
|
+
return actions.includes('check') ? 'check' : actions[0];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (actions.includes('raise') && score >= raiseThreshold) return 'raise';
|
|
472
|
+
if (score >= defendThreshold || (actions.includes('call') && score >= 0.4 && toCall <= BIG_BLIND * 2)) return 'call';
|
|
473
|
+
return actions.includes('fold') ? 'fold' : 'call';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const strength = postflopStrength(game.house.hole, game.community);
|
|
477
|
+
if (toCall === 0) {
|
|
478
|
+
if ((strength.equity >= 0.72 || (strength.draw.flushDraw && strength.equity >= 0.48)) && actions.includes('bet')) {
|
|
479
|
+
return 'bet';
|
|
480
|
+
}
|
|
481
|
+
return actions.includes('check') ? 'check' : actions[0];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (actions.includes('raise') && strength.equity >= Math.max(0.78, potOdds + 0.28)) {
|
|
485
|
+
return 'raise';
|
|
486
|
+
}
|
|
487
|
+
if (actions.includes('call') && strength.equity >= Math.max(0.24, potOdds * 0.95)) {
|
|
488
|
+
return 'call';
|
|
489
|
+
}
|
|
490
|
+
return actions.includes('fold') ? 'fold' : 'call';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function resetStreet(game) {
|
|
494
|
+
game.player.bet = 0;
|
|
495
|
+
game.house.bet = 0;
|
|
496
|
+
game.currentBet = 0;
|
|
497
|
+
game.raisesThisStreet = 0;
|
|
498
|
+
game.actedThisStreet = { player: false, house: false };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function setCurrentActor(game, actor) {
|
|
502
|
+
game.currentActor = actor;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function settleRealPayout(game) {
|
|
506
|
+
if (game.mode !== 'real' || game.winner !== 'player') return;
|
|
507
|
+
const data = await game.deps.payoutWin({
|
|
508
|
+
gameId: game.id,
|
|
509
|
+
buyInUsdc: game.buyInUsdc,
|
|
510
|
+
payoutUsdc: game.payoutUsdc,
|
|
511
|
+
agentWallet: game.agentWallet,
|
|
512
|
+
hand: game.summary,
|
|
513
|
+
}, game);
|
|
514
|
+
game.payment = {
|
|
515
|
+
...game.payment,
|
|
516
|
+
payoutTxHash: data?.payoutTxHash || data?.txHash || null,
|
|
517
|
+
payoutReceipt: data || null,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function finishGame(game, winner, reason) {
|
|
522
|
+
game.street = 'finished';
|
|
523
|
+
game.currentActor = null;
|
|
524
|
+
game.winner = winner;
|
|
525
|
+
game.result = reason;
|
|
526
|
+
|
|
527
|
+
const winnerSeat = winner === 'player' ? game.player : game.house;
|
|
528
|
+
winnerSeat.stack += game.pot;
|
|
529
|
+
|
|
530
|
+
if (reason === 'showdown') {
|
|
531
|
+
const playerEval = evaluateHand([...game.player.hole, ...game.community]);
|
|
532
|
+
const houseEval = evaluateHand([...game.house.hole, ...game.community]);
|
|
533
|
+
game.player.hand = playerEval;
|
|
534
|
+
game.house.hand = houseEval;
|
|
535
|
+
game.summary = winner === 'player'
|
|
536
|
+
? `You win with ${playerEval.name} against ${houseEval.name}`
|
|
537
|
+
: `House wins with ${houseEval.name} against ${playerEval.name}`;
|
|
538
|
+
} else {
|
|
539
|
+
game.summary = winner === 'player' ? 'House folded' : 'You folded';
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (game.winner === 'player' || game.winner === 'house') {
|
|
543
|
+
history.unshift({
|
|
544
|
+
id: game.id,
|
|
545
|
+
mode: game.mode,
|
|
546
|
+
winner: game.winner,
|
|
547
|
+
result: game.result,
|
|
548
|
+
summary: game.summary,
|
|
549
|
+
pot: game.pot,
|
|
550
|
+
community: game.community.slice(),
|
|
551
|
+
playerHole: game.player.hole.slice(),
|
|
552
|
+
houseHole: game.house.hole.slice(),
|
|
553
|
+
payoutUsdc: game.mode === 'real' && game.winner === 'player' ? game.payoutUsdc : 0,
|
|
554
|
+
createdAt: game.createdAt,
|
|
555
|
+
completedAt: new Date().toISOString(),
|
|
556
|
+
});
|
|
557
|
+
history.splice(MAX_HISTORY);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function showdown(game) {
|
|
562
|
+
game.street = 'showdown';
|
|
563
|
+
const playerEval = evaluateHand([...game.player.hole, ...game.community]);
|
|
564
|
+
const houseEval = evaluateHand([...game.house.hole, ...game.community]);
|
|
565
|
+
game.player.hand = playerEval;
|
|
566
|
+
game.house.hand = houseEval;
|
|
567
|
+
const cmp = compareHands(playerEval, houseEval);
|
|
568
|
+
if (cmp > 0) finishGame(game, 'player', 'showdown');
|
|
569
|
+
else if (cmp < 0) finishGame(game, 'house', 'showdown');
|
|
570
|
+
else {
|
|
571
|
+
game.street = 'finished';
|
|
572
|
+
game.currentActor = null;
|
|
573
|
+
game.winner = 'push';
|
|
574
|
+
game.result = 'push';
|
|
575
|
+
const split = Math.floor(game.pot / 2);
|
|
576
|
+
game.player.stack += split;
|
|
577
|
+
game.house.stack += game.pot - split;
|
|
578
|
+
game.summary = `Split pot with ${playerEval.name}`;
|
|
579
|
+
history.unshift({
|
|
580
|
+
id: game.id,
|
|
581
|
+
mode: game.mode,
|
|
582
|
+
winner: 'push',
|
|
583
|
+
result: 'push',
|
|
584
|
+
summary: game.summary,
|
|
585
|
+
pot: game.pot,
|
|
586
|
+
community: game.community.slice(),
|
|
587
|
+
playerHole: game.player.hole.slice(),
|
|
588
|
+
houseHole: game.house.hole.slice(),
|
|
589
|
+
payoutUsdc: 0,
|
|
590
|
+
createdAt: game.createdAt,
|
|
591
|
+
completedAt: new Date().toISOString(),
|
|
592
|
+
});
|
|
593
|
+
history.splice(MAX_HISTORY);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function dealRemainingBoard(game) {
|
|
598
|
+
while (game.community.length < 5) {
|
|
599
|
+
if (game.community.length === 0) dealBoard(game, 3);
|
|
600
|
+
else dealBoard(game, 1);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function advanceStreet(game) {
|
|
605
|
+
if (game.player.folded || game.house.folded) return;
|
|
606
|
+
if (game.player.allIn || game.house.allIn) {
|
|
607
|
+
dealRemainingBoard(game);
|
|
608
|
+
showdown(game);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const next = nextStreet(game.street);
|
|
613
|
+
if (next === 'showdown') {
|
|
614
|
+
showdown(game);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
game.street = next;
|
|
619
|
+
resetStreet(game);
|
|
620
|
+
if (next === 'flop') dealBoard(game, 3);
|
|
621
|
+
else if (next === 'turn' || next === 'river') dealBoard(game, 1);
|
|
622
|
+
|
|
623
|
+
const firstActor = game.dealer === 'player' ? 'house' : 'player';
|
|
624
|
+
setCurrentActor(game, firstActor);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function applyActionInternal(game, actor, action) {
|
|
628
|
+
if (game.currentActor !== actor) {
|
|
629
|
+
throw new Error(`It is not ${actor}'s turn`);
|
|
630
|
+
}
|
|
631
|
+
const seat = activePlayer(game, actor);
|
|
632
|
+
const opp = opponent(game, actor);
|
|
633
|
+
const available = getAvailableActions(game, actor);
|
|
634
|
+
if (!available.includes(action)) {
|
|
635
|
+
throw new Error(`Action "${action}" is not available`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const toCall = getToCall(game, actor);
|
|
639
|
+
let amount = 0;
|
|
640
|
+
|
|
641
|
+
if (action === 'fold') {
|
|
642
|
+
seat.folded = true;
|
|
643
|
+
seat.lastAction = 'fold';
|
|
644
|
+
game.actionLog.push({ actor, action, street: game.street, amount: 0 });
|
|
645
|
+
finishGame(game, actor === 'player' ? 'house' : 'player', 'fold');
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (action === 'check') {
|
|
650
|
+
seat.lastAction = 'check';
|
|
651
|
+
game.actedThisStreet[actor] = true;
|
|
652
|
+
game.actionLog.push({ actor, action, street: game.street, amount: 0 });
|
|
653
|
+
} else if (action === 'call') {
|
|
654
|
+
amount = Math.min(seat.stack, toCall);
|
|
655
|
+
seat.stack -= amount;
|
|
656
|
+
seat.bet += amount;
|
|
657
|
+
seat.committed += amount;
|
|
658
|
+
if (seat.stack === 0) seat.allIn = true;
|
|
659
|
+
game.pot += amount;
|
|
660
|
+
seat.lastAction = 'call';
|
|
661
|
+
game.actedThisStreet[actor] = true;
|
|
662
|
+
game.actionLog.push({ actor, action, street: game.street, amount });
|
|
663
|
+
} else if (action === 'bet' || action === 'raise') {
|
|
664
|
+
const betSize = plannedBetSize(game, actor, action);
|
|
665
|
+
amount = Math.min(seat.stack, action === 'raise' ? toCall + betSize : betSize);
|
|
666
|
+
seat.stack -= amount;
|
|
667
|
+
seat.bet += amount;
|
|
668
|
+
seat.committed += amount;
|
|
669
|
+
if (seat.stack === 0) seat.allIn = true;
|
|
670
|
+
game.pot += amount;
|
|
671
|
+
game.currentBet = Math.max(game.currentBet, seat.bet);
|
|
672
|
+
seat.lastAction = action;
|
|
673
|
+
game.raisesThisStreet += 1;
|
|
674
|
+
game.actedThisStreet[actor] = true;
|
|
675
|
+
game.actedThisStreet[actor === 'player' ? 'house' : 'player'] = false;
|
|
676
|
+
game.actionLog.push({ actor, action, street: game.street, amount });
|
|
677
|
+
} else if (action === 'all-in') {
|
|
678
|
+
amount = seat.stack;
|
|
679
|
+
seat.stack = 0;
|
|
680
|
+
seat.bet += amount;
|
|
681
|
+
seat.committed += amount;
|
|
682
|
+
seat.allIn = true;
|
|
683
|
+
game.pot += amount;
|
|
684
|
+
seat.lastAction = 'all-in';
|
|
685
|
+
if (seat.bet > opp.bet) {
|
|
686
|
+
game.raisesThisStreet += 1;
|
|
687
|
+
game.actedThisStreet[actor] = true;
|
|
688
|
+
game.actedThisStreet[actor === 'player' ? 'house' : 'player'] = false;
|
|
689
|
+
} else {
|
|
690
|
+
game.actedThisStreet[actor] = true;
|
|
691
|
+
}
|
|
692
|
+
game.currentBet = Math.max(game.currentBet, seat.bet);
|
|
693
|
+
game.actionLog.push({ actor, action, street: game.street, amount });
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (game.street === 'finished') return;
|
|
697
|
+
|
|
698
|
+
if (seat.folded || opp.folded) return;
|
|
699
|
+
|
|
700
|
+
if (seat.allIn && opp.allIn) {
|
|
701
|
+
dealRemainingBoard(game);
|
|
702
|
+
showdown(game);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (seat.bet === opp.bet && game.actedThisStreet.player && game.actedThisStreet.house) {
|
|
707
|
+
advanceStreet(game);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
setCurrentActor(game, actor === 'player' ? 'house' : 'player');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function autoPlayHouse(game) {
|
|
715
|
+
while (game.currentActor === 'house' && game.street !== 'finished') {
|
|
716
|
+
const action = decideHouseAction(game);
|
|
717
|
+
applyActionInternal(game, 'house', action);
|
|
718
|
+
}
|
|
719
|
+
if (game.street === 'finished' && game.mode === 'real' && game.winner === 'player' && !game.payment?.payoutReceipt && !game.payment?.payoutError) {
|
|
720
|
+
try {
|
|
721
|
+
await settleRealPayout(game);
|
|
722
|
+
} catch (err) {
|
|
723
|
+
game.payment = { ...game.payment, payoutError: err.message };
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function serializeGame(game) {
|
|
729
|
+
const revealHouse = game.street === 'finished' || game.street === 'showdown';
|
|
730
|
+
return {
|
|
731
|
+
id: game.id,
|
|
732
|
+
mode: game.mode,
|
|
733
|
+
buyInUsdc: game.buyInUsdc,
|
|
734
|
+
payoutUsdc: game.payoutUsdc,
|
|
735
|
+
agentWallet: game.agentWallet || null,
|
|
736
|
+
street: game.street,
|
|
737
|
+
dealer: game.dealer,
|
|
738
|
+
currentActor: game.currentActor,
|
|
739
|
+
pot: game.pot,
|
|
740
|
+
community: game.community.slice(),
|
|
741
|
+
currentBet: Math.max(game.player.bet, game.house.bet),
|
|
742
|
+
availableActions: getAvailableActions(game, 'player'),
|
|
743
|
+
player: {
|
|
744
|
+
stack: game.player.stack,
|
|
745
|
+
bet: game.player.bet,
|
|
746
|
+
committed: game.player.committed,
|
|
747
|
+
hole: game.player.hole.slice(),
|
|
748
|
+
hand: game.player.hand ? clone(game.player.hand) : null,
|
|
749
|
+
lastAction: game.player.lastAction || null,
|
|
750
|
+
},
|
|
751
|
+
house: {
|
|
752
|
+
stack: game.house.stack,
|
|
753
|
+
bet: game.house.bet,
|
|
754
|
+
committed: game.house.committed,
|
|
755
|
+
hole: revealHouse ? game.house.hole.slice() : ['??', '??'],
|
|
756
|
+
holeHidden: !revealHouse,
|
|
757
|
+
hand: revealHouse && game.house.hand ? clone(game.house.hand) : null,
|
|
758
|
+
lastAction: game.house.lastAction || null,
|
|
759
|
+
},
|
|
760
|
+
winner: game.winner || null,
|
|
761
|
+
result: game.result || null,
|
|
762
|
+
summary: game.summary || null,
|
|
763
|
+
actionLog: game.actionLog.slice(),
|
|
764
|
+
payment: game.payment ? clone(game.payment) : null,
|
|
765
|
+
createdAt: game.createdAt,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function resolveAgentWallet(opts = {}) {
|
|
770
|
+
if (opts.wallet) return opts.wallet;
|
|
771
|
+
const activeWallet = getConfig('activeWallet');
|
|
772
|
+
if (!activeWallet) return null;
|
|
773
|
+
try {
|
|
774
|
+
const { loadWallet } = await import('../wallet/keystore.js');
|
|
775
|
+
return loadWallet(activeWallet).address;
|
|
776
|
+
} catch {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function getSignerToken() {
|
|
782
|
+
return process.env.DARKSOL_SIGNER_TOKEN || getConfig('signerToken') || null;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function buildDeps(overrides = {}) {
|
|
786
|
+
return {
|
|
787
|
+
fetchStats: async () => fetchJSON(`${getURL()}/api/stats`),
|
|
788
|
+
isSignerRunning: async (token) => isSignerRunning(token),
|
|
789
|
+
payBuyIn: async (payload) => fetchWithX402(
|
|
790
|
+
`${getURL()}/api/poker/buyin`,
|
|
791
|
+
{
|
|
792
|
+
method: 'POST',
|
|
793
|
+
headers: { 'Content-Type': 'application/json' },
|
|
794
|
+
body: JSON.stringify(payload),
|
|
795
|
+
},
|
|
796
|
+
{ signerToken: payload.signerToken, autoSign: true },
|
|
797
|
+
),
|
|
798
|
+
payoutWin: async (payload) => fetchJSON(`${getURL()}/api/poker/payout`, {
|
|
799
|
+
method: 'POST',
|
|
800
|
+
headers: { 'Content-Type': 'application/json' },
|
|
801
|
+
body: JSON.stringify(payload),
|
|
802
|
+
}),
|
|
803
|
+
...overrides,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async function prepareRealMode(game, opts) {
|
|
808
|
+
const stats = await game.deps.fetchStats();
|
|
809
|
+
const houseBalance = Number(stats?.houseBalanceUsdc || 0);
|
|
810
|
+
if (houseBalance < game.payoutUsdc) {
|
|
811
|
+
throw new Error('House balance too low for payouts. Try free mode!');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const signerToken = getSignerToken();
|
|
815
|
+
const signerUp = await game.deps.isSignerRunning(signerToken);
|
|
816
|
+
if (!signerUp) {
|
|
817
|
+
throw new Error('Agent signer is required for real-mode poker. Start it with: darksol agent start <wallet-name>');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const agentWallet = await resolveAgentWallet(opts);
|
|
821
|
+
if (!agentWallet) {
|
|
822
|
+
throw new Error('A wallet address is required for real-mode poker payouts.');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const payment = await game.deps.payBuyIn({
|
|
826
|
+
gameId: game.id,
|
|
827
|
+
mode: game.mode,
|
|
828
|
+
buyInUsdc: game.buyInUsdc,
|
|
829
|
+
agentWallet,
|
|
830
|
+
signerToken,
|
|
831
|
+
}, game);
|
|
832
|
+
|
|
833
|
+
if (payment?.error) {
|
|
834
|
+
throw new Error(payment.error);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
game.agentWallet = agentWallet;
|
|
838
|
+
game.payment = {
|
|
839
|
+
paid: !!payment?.paid,
|
|
840
|
+
paymentReceipt: payment?.data || null,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
export async function pokerNewGame(opts = {}) {
|
|
845
|
+
const mode = opts.mode === 'real' ? 'real' : 'free';
|
|
846
|
+
const id = opts.gameId || randomUUID();
|
|
847
|
+
const dealer = opts.dealer || dealerToggle;
|
|
848
|
+
dealerToggle = dealerToggle === 'player' ? 'house' : 'player';
|
|
849
|
+
|
|
850
|
+
const deck = opts.deck ? opts.deck.slice() : shuffleDeck(createDeck(), opts.random || Math.random);
|
|
851
|
+
const game = {
|
|
852
|
+
id,
|
|
853
|
+
mode,
|
|
854
|
+
buyInUsdc: Number(opts.buyInUsdc || DEFAULT_BUY_IN_USDC),
|
|
855
|
+
payoutUsdc: Number(opts.payoutUsdc || DEFAULT_PAYOUT_USDC),
|
|
856
|
+
createdAt: new Date().toISOString(),
|
|
857
|
+
dealer,
|
|
858
|
+
street: 'preflop',
|
|
859
|
+
currentActor: null,
|
|
860
|
+
pot: 0,
|
|
861
|
+
currentBet: BIG_BLIND,
|
|
862
|
+
raisesThisStreet: 0,
|
|
863
|
+
actedThisStreet: { player: false, house: false },
|
|
864
|
+
community: [],
|
|
865
|
+
deck,
|
|
866
|
+
player: { stack: STARTING_STACK, bet: 0, committed: 0, hole: [], folded: false, allIn: false, lastAction: null, hand: null },
|
|
867
|
+
house: { stack: STARTING_STACK, bet: 0, committed: 0, hole: [], folded: false, allIn: false, lastAction: null, hand: null },
|
|
868
|
+
actionLog: [],
|
|
869
|
+
winner: null,
|
|
870
|
+
result: null,
|
|
871
|
+
summary: null,
|
|
872
|
+
payment: null,
|
|
873
|
+
agentWallet: null,
|
|
874
|
+
deps: buildDeps(opts.deps),
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
if (mode === 'real') {
|
|
878
|
+
await prepareRealMode(game, opts);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
dealHoleCards(game);
|
|
882
|
+
|
|
883
|
+
if (dealer === 'player') {
|
|
884
|
+
postBlind(game, 'player', SMALL_BLIND);
|
|
885
|
+
postBlind(game, 'house', BIG_BLIND);
|
|
886
|
+
setCurrentActor(game, 'player');
|
|
887
|
+
} else {
|
|
888
|
+
postBlind(game, 'house', SMALL_BLIND);
|
|
889
|
+
postBlind(game, 'player', BIG_BLIND);
|
|
890
|
+
setCurrentActor(game, 'house');
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
games.set(id, game);
|
|
894
|
+
await autoPlayHouse(game);
|
|
895
|
+
return pokerStatus(id);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
export async function pokerAction(gameId, action) {
|
|
899
|
+
const game = games.get(gameId);
|
|
900
|
+
if (!game) throw new Error(`Poker game not found: ${gameId}`);
|
|
901
|
+
if (game.street === 'finished') return pokerStatus(gameId);
|
|
902
|
+
if (game.currentActor !== 'player') throw new Error('It is not your turn');
|
|
903
|
+
|
|
904
|
+
applyActionInternal(game, 'player', String(action || '').toLowerCase());
|
|
905
|
+
await autoPlayHouse(game);
|
|
906
|
+
return pokerStatus(gameId);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export function pokerStatus(gameId) {
|
|
910
|
+
if (!gameId) {
|
|
911
|
+
const active = [...games.values()].find(game => game.street !== 'finished');
|
|
912
|
+
return active ? serializeGame(active) : null;
|
|
913
|
+
}
|
|
914
|
+
const game = games.get(gameId);
|
|
915
|
+
return game ? serializeGame(game) : null;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
export function pokerHistory() {
|
|
919
|
+
return history.map(item => clone(item));
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
export const __test = {
|
|
923
|
+
createDeck,
|
|
924
|
+
shuffleDeck,
|
|
925
|
+
evaluateFiveCards,
|
|
926
|
+
evaluateHand,
|
|
927
|
+
compareHands,
|
|
928
|
+
holeStrength,
|
|
929
|
+
postflopStrength,
|
|
930
|
+
decideHouseAction,
|
|
931
|
+
getAvailableActions,
|
|
932
|
+
resetState: () => {
|
|
933
|
+
games.clear();
|
|
934
|
+
history.length = 0;
|
|
935
|
+
dealerToggle = 'player';
|
|
936
|
+
},
|
|
937
|
+
};
|