Skip to content

Commit

Permalink
Merge pull request #5 from CharlieHess/stacks-on-stacks
Browse files Browse the repository at this point in the history
Chip Stacks and Pot Calculations
  • Loading branch information
CharlieHess committed Aug 2, 2015
2 parents 5e65fdf + a069a9d commit 29ebebf
Show file tree
Hide file tree
Showing 8 changed files with 501 additions and 125 deletions.
15 changes: 12 additions & 3 deletions ai/aggro-bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ class AggroBot {

// TOO STRONK
getAction(availableActions, previousActions) {
let action = availableActions.indexOf('raise') > -1 ? 'raise' : 'bet';
let delay = 1000 + (Math.random() * 500);
return rx.Observable.timer(delay).map(() => action);
let desiredAction = 'call';

if (availableActions.indexOf('raise') > -1) {
desiredAction = 'raise';
} else if (availableActions.indexOf('bet') > -1) {
desiredAction = 'bet';
}

let delay = 2000 + (Math.random() * 2000);
return rx.Observable.timer(delay).map(() => {
return { name: desiredAction };
});
}
};
6 changes: 4 additions & 2 deletions ai/weak-bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ class WeakBot {

// LOL WEAK
getAction(availableActions, previousActions) {
let action = availableActions.indexOf('check') > -1 ? 'check' : 'call';
let delay = 1000 + (Math.random() * 3000);
let action = availableActions.indexOf('check') > -1 ?
{ name: 'check' } :
{ name: 'call' };
let delay = 2000 + (Math.random() * 4000);
return rx.Observable.timer(delay).map(() => action);
}
};
93 changes: 61 additions & 32 deletions src/player-interaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ class PlayerInteraction {
// `onCompleted` when time expires or the max number of players join.
static pollPotentialPlayers(messages, channel, scheduler=rx.Scheduler.timeout, timeout=5, maxPlayers=6) {
let intro = `Who wants to play?`;
let formatMessage = (t) => `Respond with 'yes' in this channel in the next ${t} seconds.`;
let formatMessage = t => `Respond with 'yes' in this channel in the next ${t} seconds.`;
let {timeExpired} = PlayerInteraction.postMessageWithTimeout(channel, intro,
formatMessage, scheduler, timeout);

// Look for messages containing the word 'yes' and map them to a unique
// user ID, constrained to `maxPlayers` number of players.
let newPlayers = messages.where((e) => e.text && e.text.toLowerCase().match(/\byes\b/))
.map((e) => e.user)
let newPlayers = messages.where(e => e.text && e.text.toLowerCase().match(/\byes\b/))
.map(e => e.user)
.distinct()
.take(maxPlayers)
.publish();
Expand All @@ -45,36 +45,38 @@ class PlayerInteraction {
//
// Returns an {Observable} indicating the action the player took. If time
// expires, a 'timeout' action is returned.
static getActionForPlayer(messages, channel, player, previousActions, scheduler=rx.Scheduler.timeout, timeout=30) {
static getActionForPlayer(messages, channel, player, previousActions,
scheduler=rx.Scheduler.timeout, timeout=30) {
let intro = `${player.name}, it's your turn to act.`;
let availableActions = PlayerInteraction.getAvailableActions(player, previousActions);
let formatMessage = (t) => PlayerInteraction.buildActionMessage(availableActions, t);
let formatMessage = t => PlayerInteraction.buildActionMessage(availableActions, t);
let {timeExpired} = PlayerInteraction.postMessageWithTimeout(channel, intro,
formatMessage, scheduler, timeout);

// Look for text that conforms to a player action.
let playerAction = messages.where((e) => e.user === player.id)
.map((e) => PlayerInteraction.actionForMessage(e.text, availableActions))
.where((action) => action !== '')
let playerAction = messages.where(e => e.user === player.id)
.map(e => PlayerInteraction.actionFromMessage(e.text, availableActions))
.where(action => action !== null)
.publish();

playerAction.connect();
let disp = timeExpired.connect();

// If the user times out, they will be auto-folded unless they can check.
let actionForTimeout = timeExpired.map(() =>
availableActions.indexOf('check') > -1 ? 'check' : 'fold');
availableActions.indexOf('check') > -1 ?
{ name: 'check' } :
{ name: 'fold' });

let botAction = player.isBot ?
player.getAction(availableActions, previousActions) :
rx.Observable.never();

// NB: Take the first result from the player action, the timeout, and a bot
// action (only applicable to bots).
return rx.Observable
.merge(playerAction, actionForTimeout,
player.isBot ? player.getAction(availableActions, previousActions) : rx.Observable.never())
return rx.Observable.merge(playerAction, actionForTimeout, botAction)
.take(1)
.do((action) => {
disp.dispose();
channel.send(`${player.name} ${action}s.`);
});
.do(() => disp.dispose());
}

// Private: Posts a message to the channel with some timeout, that edits
Expand Down Expand Up @@ -127,54 +129,81 @@ class PlayerInteraction {
// Returns an array of strings
static getAvailableActions(player, previousActions) {
let actions = _.values(previousActions);
let playerBet = actions.indexOf('bet') > -1;
let playerRaised = actions.indexOf('raise') > -1;
let betActions = _.filter(actions, a => a.name === 'bet' || a.name === 'raise');
let hasBet = betActions.length > 0;

let availableActions = [];

if (player.hasOption) {
availableActions.push('check');
availableActions.push('raise');
} else if (playerBet || playerRaised) {
} else if (hasBet) {
availableActions.push('call');
availableActions.push('raise');
} else {
availableActions.push('check');
availableActions.push('bet');
}

// Prevent players from raising when they don't have enough chips.
let raiseIndex = availableActions.indexOf('raise');
if (raiseIndex > -1 &&
_.max(betActions, a => a.amount).amount >= player.chips) {
availableActions.splice(raiseIndex, 1);
}

availableActions.push('fold');
return availableActions;
}

// Private: Maps abbreviated text for a player action to its canonical name.
// Private: Parse player input into a valid action.
//
// text - The text of the player message
// text - The text that the player entered
// availableActions - An array of the actions available to this player
//
// Returns the canonical action
static actionForMessage(text, availableActions) {
if (!text) return '';
// Returns an object representing the action, with keys for the name and
// bet amount, or null if the input was invalid.
static actionFromMessage(text, availableActions) {
if (!text) return null;

switch (text.toLowerCase()) {
let input = text.trim().toLowerCase().split(/\s+/);
if (!input[0]) return null;

let name = '';
let amount = 0;

switch (input[0]) {
case 'c':
return availableActions[0];
name = availableActions[0];
break;
case 'call':
return 'call';
name = 'call';
break;
case 'check':
return 'check';
name = 'check';
break;
case 'f':
case 'fold':
return 'fold';
name = 'fold';
break;
case 'b':
case 'bet':
return 'bet';
name = 'bet';
amount = input[1] ? parseInt(input[1]) : NaN;
break;
case 'r':
case 'raise':
return 'raise';
name = 'raise';
amount = input[1] ? parseInt(input[1]) : NaN;
break;
default:
return '';
return null;
}

// NB: Unavailable actions are always invalid.
return availableActions.indexOf(name) > -1 ?
{ name: name, amount: amount } :
null;
}
}

Expand Down
18 changes: 17 additions & 1 deletion src/player-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ module.exports = class PlayerOrder {
// comparing indices.
//
// actingPlayer - The player who is acting
// players - A list of all players in the hand, sorted by position
// players - An array of all players in the hand, sorted by position
//
// Returns true if this player is the last to act, false otherwise
static isLastToAct(actingPlayer, players) {
Expand All @@ -59,4 +59,20 @@ module.exports = class PlayerOrder {
actingPlayer === playerWithOption :
(currentIndex + 1) % playersRemaining.length === bettorIndex;
}

// Public: Returns the index of the next player to act.
//
// index - The current index
// players - An array of all players in the hand, sorted by position
//
// Returns the index of the next player in the hand
static getNextPlayerIndex(index, players) {
let player = null;
do {
index = (index + 1) % players.length;
player = players[index];
} while (!player.isInHand);

return index;
}
};
34 changes: 22 additions & 12 deletions src/player-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ const textTable = require('text-table');

class PlayerStatus {
// Public: Displays a fixed-width text table showing all of the players in
// the hand, relevant position information (blinds, dealer button),
// the game, relevant position information (blinds, dealer button),
// information about the player's bet, and an indicator of who's next to act.
//
// channel - The channel where the status message will be displayed
// players - The players in the hand
// players - The players in the game
// actingPlayer - The player taking action
// currentPot - The total amount of chips in the pot
// dealerButton - The index of the dealer button
// bigBlind - The index of the big blind
// smallBlind - The index of the small blind
Expand All @@ -16,7 +17,7 @@ class PlayerStatus {
//
// Returns nothing
static displayHandStatus(channel, players, actingPlayer,
dealerButton, bigBlind, smallBlind, tableFormatter=`\`\`\``) {
currentPot, dealerButton, bigBlind, smallBlind, tableFormatter=`\`\`\``) {
let table = [];

for (let idx = 0; idx < players.length; idx++) {
Expand All @@ -25,25 +26,34 @@ class PlayerStatus {
let player = players[idx];
let turnIndicator = player === actingPlayer ? '→ ' : ' ';
row.push(`${turnIndicator}${player.name}`);
row.push(`$${player.chips}`);

let handIndicator = player.isInHand ? '🂠' : ' ';
row.push(handIndicator);

let dealerIndicator = idx === dealerButton ? 'Ⓓ' : ' ';
row.push(dealerIndicator);

let bigBlindText = idx === bigBlind ? 'Ⓑ' : null;
let dealerText = idx === dealerButton ? 'Ⓓ' : null;
let smallBlindText = idx === smallBlind ? 'Ⓢ' : null;
let blindIndicator = bigBlindText || smallBlindText || ' ';
row.push(blindIndicator);

row.push(player.lastAction || '');
let bigBlindText = idx === bigBlind ? 'Ⓑ' : null;
let positionIndicator = bigBlindText || smallBlindText || dealerText || ' ';
row.push(positionIndicator);

if (player.lastAction) {
let actionIndicator = player.lastAction.name;
if (actionIndicator === 'bet' || actionIndicator === 'raise') {
actionIndicator += ` $${player.lastAction.amount}`;
}
row.push(actionIndicator);
} else {
row.push('');
}

table.push(row);
}

let fixedWidthTable = `${tableFormatter}${textTable(table)}${tableFormatter}`;
channel.send(fixedWidthTable);
let handStatus = `${fixedWidthTable}\nPot: $${currentPot}`;

channel.send(handStatus);
}
}

Expand Down
Loading

0 comments on commit 29ebebf

Please sign in to comment.