Repository navigation
Expand file tree
/
Copy pathActor.js
More file actions
405 lines (327 loc) · 9.96 KB
/
Copy pathActor.js
File metadata and controls
405 lines (327 loc) · 9.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
import { Helper } from '@skypager/runtime'
import Range, { SYMBOLS } from './Range'
import RangeEquity from './RangeEquity'
/**
* The Actor class represents a player at a table, and is intended to make actions
* in various situations based on the assigned strategy parameters for the actor
*/
export class Actor extends Helper {
static isCacheable = true
static isObservable = true
static attach = attach
static observables() {
return {
ranges: ["shallowMap", {}],
defineRange: ["action", this.defineRange]
}
}
static didCreateHelper(runtime, actor) {
actor.setup()
return actor
}
/**
* The purpose of the actor, of course, is to act. Calling this method will give the actor
* an opportunity to use all of the situational and environmental information available to
* make a decision from one of the available options presented by the game.
*
* An actor uses its generateDecision method to come up with the object to send to the game.
*/
async act(options = {}) {
if (!this.inHand) {
return false
}
if (options.action) {
const action = { ...options, playerId: this.playerId }
this.game.recordAction(action)
return action
}
const result = await this.generateDecision(options)
// TODO: Add validation of the action and amount
this.game.recordAction(result)
return result
}
/**
* Generate a decision that can be sent to the game.
*
* @private
*/
async generateDecision(options = {}) {
const makeDecision = this.tryGet('makeDecision')
const { action, amount } = await makeDecision.call(this, options, this.context)
const result = { playerId: this.playerId, action, amount };
return result
}
/**
* Setup calls the hooks for an Actor implementation module to define the parameters
* for an actors automatic decision making process. You can define ranges for each position,
* and each stage. Generally this is the most useful for defining preflop hand selection
* and strategy (e.g. open raise, 3bet, 4bet, steal, squeeze play, etc)
*
* Post-flop decision making will need to be more dynamic, so you can control this through
* the findRange hook
*/
setup() {
this.tryResult('defineRanges')
return this
}
/**
* Will attempt to use the findRange hook to find the Range that governs the actors
* hand selection choice for the current stage and position. If there is no hook,
* or a Range is not returned, it will attempt to check for defined ranges that make sense
* (e.g. in an open pot, preflop, your opening range will determine if you're going to raise.)
*
* If you don't have an opening range defined, but a calling range, this would be your limping range.
*/
findRangeForSituation() {
if (this.tryGet('findRange')) {
const found = this.tryResult('findRange')
if (found) {
return this.toRange(found)
}
}
const { isPotRaised, isPotOpen, stage } = this.game
const { activeOpenRange, activeCallingRange } = this
if (stage === 'preflop' && isPotOpen && activeOpenRange) {
return activeOpenRange
} else if (isPotRaised && activeCallingRange) {
return activeCallingRange
}
}
/**
* Returns a tree of stages, positions, and actions,
* and the ranges of hands that are in each.
*/
get rangeSettings() {
const { keyBy, groupBy, mapValues } = this.lodash
const ranges = this.ranges.values()
return mapValues(
groupBy(ranges, 'stage'),
(rangesAtStage) =>
mapValues(groupBy(rangesAtStage, 'position'), (actions) => keyBy(actions, 'action')))
}
/**
* Returns a single range that for that situation
*/
get activeRange() {
const activeRange = this.findRangeForSituation()
return activeRange && this.toRange(activeRange)
}
get activeCallingRange() {
if (this.activeRanges.call) {
return this.toRange(this.activeRanges.call, this.deadCards, this.playersLeftInHand)
}
const callingRange = this.tryResult('callingRange')
return callingRange && this.toRange(callingRange)
}
get activeOpenRange() {
if (this.activeRanges.open) {
return this.toRange(this.activeRanges.open, this.deadCards, this.playersLeftInHand)
}
const openRange = this.tryResult('openRange')
return openRange && this.toRange(openRange)
}
get currentRanges() {
return this.get(['rangeSettings', this.game.stage, this.position], {})
}
get activeRanges() {
const { availableOptions } = this
const { pick } = this.lodash
return pick(this.currentRanges, availableOptions)
}
get continueRanges() {
const { omit } = this.lodash
return omit(this.activeRanges, 'fold')
}
toRange(input) {
if (typeof input === 'string') {
return new Range(input, this.deadCards, this.numberOfPlayersInHand)
} else if (typeof input === 'object' && !input.combos && typeof input.range === 'string') {
return new Range(input.range, this.deadCards, this.numberOfPlayersInHand)
} else if (typeof input === 'object' && input.combos) {
return input
}
}
defineRange(options = {}) {
const { isEmpty } = this.lodash
const { stage = this.stage, position = this.position, actions = {}, action, range = ''} = options
if (isEmpty(action) && !isEmpty(actions)) {
return Object.keys(actions).map(action =>
this.defineRange({ action, stage, position, action, range: actions[action] })
)
}
const key = `${stage}:${action}:${position}`
this.ranges.set(key, {
range,
action,
position,
stage,
})
return this.ranges.get(key)
}
get handInRange() {
return this.activeRange && this.activeRange.includes(this.holding)
}
get handInCallingRange() {
return this.activeCallingRange && this.activeCallingRange.includes(this.holding)
}
get handInOpeningRange() {
return this.activeOpenRange && this.activeOpenRange.includes(this.holding)
}
/**
* Which actions have already taken place from the perspective of the actor
*/
get previousActions() {
return this.game.stageActions
}
get toGo() {
return this.game.toGo - this.amountInvestedThisRound
}
get toGoAsPercentOfStack() {
return (this.toGo / this.chips) * 100
}
get effectiveStackSize() {
return this.game.effectiveStackSize
}
get potOdds() {
return parseFloat((this.game.pot / this.toGo).toFixed(2))
}
get amountInvested() {
return this
.game.chain
.get('actions')
.filter({ playerId: this.playerId })
.map(a => a.amount || 0)
.sum()
.value()
}
get deadCards() {
return this.game.board.map(card => card.name)
}
get bigBlindsLeft() {
return Math.floor(this.chips / this.game.bigBlindAmount)
}
get isLastToAct() {
return this.game.actionOrder.indexOf(this.seat) === this.game.actionOrder.length - 1
}
get availableOptions() {
const { uniq } = this.lodash
const availableOptions = this.game.availableOptions
if (this.toGo === 0) {
return uniq(['check', ...availableOptions])
}
return availableOptions
}
get amountInvestedThisRound() {
return this
.game.chain
.get('stageActions')
.filter({ playerId: this.playerId })
.map(a => a.amount || 0)
.sum()
.value()
}
get allIn() {
return !!this.playerData.allIn
}
get inHand() {
return !!this.playerData.inHand
}
get chips() {
return this.playerData.chips
}
get cards() {
return this.playerData.cards
}
get seat() {
return this.playerData.seat
}
get preflopPosition() {
const { seat, game } = this
const { dealerSeat, actionOrder = [] } = game
if (seat === dealerSeat) {
return 'BTN'
}
const dealerPos = actionOrder.indexOf(dealerSeat)
const pos = actionOrder.indexOf(seat)
if (pos === 1) {
return 'BB'
} else if (pos === 0) {
return 'SB'
} else if (pos === dealerPos - 1) {
return 'CO'
} else if (pos === dealerPos - 2) {
return 'HJ'
} else if (pos === 2) {
return 'UTG'
} else if (pos === 3) {
return 'UTG+1'
} else if (pos === 4) {
return 'MP'
} else if (pos === 5) {
return 'MP+1'
} else if (pos === 6) {
return 'MP+2'
} else if (pos === 7) {
return 'MP+3'
}
}
get position() {
if (this.game.stage === 'preflop') {
return this.preflopPosition
}
return this.game.actionOrder.length - this.game.actionOrder.indexOf(this.seat)
}
get holding() {
const { sortBy } = this.lodash
const { cards } = this
if (!cards.length) return
return sortBy(this.cards, 'rank', 'suit').reverse().map(c => c.name).join("")
}
get playersBehind() {
const { seat } = this
if (!this.inHand) {
return -1
}
const { actionOrderInHand } = this.game
const index = actionOrderInHand.indexOf(seat)
return actionOrderInHand.slice(index).length
}
get playersLeftInHand() {
return this.game.numberOfPlayersInHand
}
get combo() {
return Range.combosMap.get(this.holding) || Range.combos.find(c => c.name === this.holding)
}
get actualHandRange() {
return new Range(this.combo.normalized, this.deadCards, this.playersLeftInHand)
}
get actualHandRangeStrength() {
return this.actualHandRange.strength
}
get preflopStrength() {
return this.game.playerPreflopStrength[this.playerId]
}
get profile() {
return this.name
}
get playerId() {
return this.tryGet('playerId')
}
get game() {
return this.tryGet('game')
}
get playerData() {
return this.game.playerData.get(this.playerId)
}
}
export default Actor
export function attach(runtime) {
Helper.registerHelper("actor", () => Actor);
Helper.attach(runtime, Actor, {
registry: Helper.createContextRegistry("actors", {
context: Helper.createMockContext({})
}),
lookupProp: "actor",
registryProp: "actors"
});
}