11
\$\begingroup\$

I'm building a simple Poker game. It is almost complete, but I want to refactor it early.

My code is mainly about game-play, determine if ready to start next round, determine whose turn it is, etc. I want to make this code more readable, easy to maintain, and not too difficult to understand for other programmers. Any suggestions?

index.js (usage)

var Game = require('./src/game');
var game = new Game();

game.addPlayer({
    name: "A",
    chips: 40000
});
game.addPlayer({
    name: "B",
    chips: 20000
});
game.addPlayer({
    name: "C",
    chips: 20000
});

game.start();

game.getCurrentPlayer().callOrCheck();      // A
game.getCurrentPlayer().callOrCheck();      // B
game.getCurrentPlayer().raise(2000);        // C
game.getCurrentPlayer().raise(2000);        // A
game.getCurrentPlayer().fold();             // B
game.getCurrentPlayer().callOrCheck();      // C
game.getCurrentPlayer().callOrCheck();      // A
game.getCurrentPlayer().raise(1000);        // C
game.getCurrentPlayer().callOrCheck();      // A
game.getCurrentPlayer().callOrCheck();      // C
game.getCurrentPlayer().raise(3000);        // A
game.getCurrentPlayer().callOrCheck();      // C
game.getCurrentPlayer().callOrCheck();      // A
game.getCurrentPlayer().callOrCheck();      // C

Results in console:

Player A added to the game
Player B added to the game
Player C added to the game
Game reset
========== STARTING GAME ==========
    Player A gets card : As & 4c
Player B gets card : Th & 2s
Player C gets card : 5c & 7s
Player A is the dealer
Player B pays small blind : 1000
Player C pays big blind : 2000
Now its player A's turn
========== Round DEAL ==========
    Player A CALL : 2000
cannot begin next round
Player B CALL : 1000
cannot begin next round
Player C Raises : 2000
cannot begin next round
Player A Raises : 4000
cannot begin next round
Player B FOLD
cannot begin next round
Player C CALL : 2000
begin next round
Total Pot : 14000
========== Round FLOP ==========
    Community cards : Ad, Qh, 6h
Player A CHECK
cannot begin next round
Player C Raises : 1000
cannot begin next round
Player A CALL : 1000
begin next round
Total Pot : 16000
========== Round TURN ==========
    Community cards : Ad, Qh, 6h, 3d
Player C CHECK
cannot begin next round
Player A Raises : 3000
cannot begin next round
Player C CALL : 3000
begin next round
Total Pot : 22000
========== Round RIVER ==========
    Community cards : Ad, Qh, 6h, 3d, 8d
Player A CHECK
cannot begin next round
Player C CHECK
begin next round
Total Pot : 22000
========== SHOWDOWN ==========
    Community cards : Ad, Qh, 6h, 3d, 8d
Player A : As, 4c | strength : 10972 | one pair
Player B : Th, 2s | strength : 5165 | high card
Player C : 5c, 7s | strength : 5124 | high card
Player A wins with one pair

game.js

module.exports = Game;

var Player = require('./player.js');
var Deck = require('./deck.js');
var PokerEvaluator = require('poker-evaluator');

var debug = true;
function logd(message) {
    if (debug) {
        console.log(message);
    }
}

function Game() {
    // Game attributes
    this.BET = 2000;

    this.players = [];          // array of Player object, represents all players in this game
    this.round = 'idle';        // current round in a game
    this.dealerPos = 0;         // to determine the dealer position for each game, incremented by 1 for each end game
    this.turnPos = 0;           // to determine whose turn it is in a playing game
    this.pot = 0;               // accumulated chips in center of the table
    this.communityCards = [];   // array of Card object, five cards in center of the table
    this.deck = new Deck();     // deck of playing cards
}

/**
 * Adds new player to the game
 * @param attr
 */
Game.prototype.addPlayer = function(attr) {
    var newPlayer = new Player(attr);
    logd('Player ' + newPlayer.name + ' added to the game');
    newPlayer.game = this;
    this.players.push(newPlayer);
};

/**
 * Resets game to the default state
 */
Game.prototype.reset = function() {
    logd('Game reset');
    this.round = 'idle';
    this.communityCards = [];   // clear cards on board
    this.pot = 0;               // clear pots on board
    this.deck = new Deck();     // use new deck of cards
    for (var i=0; i<this.players.length; i++) {
        this.players[i].reset();
    }
};

/**
 * Starts the 'deal' Round
 */
Game.prototype.start = function() {
    this.reset();
    logd('========== STARTING GAME ==========');

    // deal two cards to each players
    for (var i=0; i<this.players.length; i++) {
        var c1 = this.deck.drawCard();
        var c2 = this.deck.drawCard();
        logd('Player ' + this.players[i].name + ' gets card : ' + c1 + ' & ' + c2);
        this.players[i].firstCard = c1;
        this.players[i].secondCard = c2;
    }

    // determine dealer, small blind, big blind
    // modulus with total number of players
    // numbers will back to 0 if exceeds the number of players
    logd('Player ' + this.players[this.dealerPos].name + ' is the dealer');
    var smallBlindPos = ( this.dealerPos+1 ) % this.players.length;
    var bigBlindPos = ( this.dealerPos+2 ) % this.players.length;

    // small and big pays blind
    this.players[smallBlindPos].addBet(1/2 * this.BET);
    this.players[bigBlindPos].addBet(this.BET);

    logd('Player ' + this.players[smallBlindPos].name + ' pays small blind : ' + (1/2 * this.BET));
    logd('Player ' + this.players[bigBlindPos].name + ' pays big blind : ' + this.BET);

    // determine whose turn it is
    this.turnPos = ( bigBlindPos+1 ) % this.players.length;
    logd('Now its player ' + this.players[this.turnPos].name + '\'s turn');

    // begin game, start 'deal' Round
    logd('========== Round DEAL ==========');
    this.round = 'deal';
};

Game.prototype.incrementPlayerTurn = function() {
    do {
        this.turnPos = ( this.turnPos+1 ) % this.players.length;
    } while(this.players[this.turnPos].hasDone);
};

/**
 * Check if ready to begin new round
 * Round ends when all players' bet are equal,
 * With exception Fold and All-in players
 * @returns {boolean}
 */
Game.prototype.isEndRound = function() {
    var endOfRound = true;
    //For each player, check
    for(var i=0; i<this.players.length; i++) {
        var plyr = this.players[i];
        if (!plyr.hasActed && !plyr.hasDone) {
            endOfRound = false;
        }
    }
    return endOfRound;
};

/**
 * Play the next round
 */
Game.prototype.nextRound = function() {
    if (this.round === 'idle') {
        this.start();
    } else if (this.round === 'deal') {
        this.gatherBets();
        this.flop();
    } else if (this.round === 'flop') {
        this.gatherBets();
        this.turn();
    } else if (this.round === 'turn') {
        this.gatherBets();
        this.river();
    } else if (this.round === 'river') {
        this.gatherBets();
        this.showdown();
    } else {
        this.start();
    }
};

/**
 * Checks if ready to next round
 * If yes, starts the next round
 */
Game.prototype.checkForNextRound = function() {
    if (this.isEndRound()) {
        logd('begin next round');
        this.nextRound();
    } else {
        logd('cannot begin next round');
    }
};

/**
 * Starts the 'flop' Round
 */
Game.prototype.flop = function() {
    logd('========== Round FLOP ==========');
    this.round = 'flop';
    // deal three cards in board
    this.communityCards[0] = this.deck.drawCard();
    this.communityCards[1] = this.deck.drawCard();
    this.communityCards[2] = this.deck.drawCard();
    // begin betting
    logd('Community cards : ' + this.communityCards[0] + ', ' + this.communityCards[1] + ', ' + this.communityCards[2]);
    // other players must act
    this.requestPlayerAction();
};

/**
 * Starts the 'turn' Round
 */
Game.prototype.turn = function() {
    logd('========== Round TURN ==========');
    this.round = 'turn';
    // deal fourth card
    this.communityCards[3] = this.deck.drawCard();
    // begin betting
    logd('Community cards : ' + this.communityCards[0] + ', ' + this.communityCards[1] + ', ' + this.communityCards[2] + ', ' + this.communityCards[3]);
    // other players must act
    this.requestPlayerAction();
};

/**
 * Starts the 'river' Round
 */
Game.prototype.river = function() {
    logd('========== Round RIVER ==========');
    this.round = 'river';
    // deal fifth card
    this.communityCards[4] = this.deck.drawCard();
    // begin betting
    logd('Community cards : ' + this.communityCards[0] + ', ' + this.communityCards[1] + ', ' + this.communityCards[2] + ', ' + this.communityCards[3] + ', '  + this.communityCards[4]);
    // other players must act
    this.requestPlayerAction();
};

/**
 * Starts the 'showdown' Round
 */
Game.prototype.showdown = function() {
    logd('========== SHOWDOWN ==========');
    this.round = 'showdown';
    // gather all hands
    var hands = [];
    for (var i=0; i<this.players.length; i++) {
        hands.push([
            this.players[i].firstCard,
            this.players[i].secondCard,
            this.communityCards[0],
            this.communityCards[1],
            this.communityCards[2],
            this.communityCards[3],
            this.communityCards[4]
        ]);
    }
    // evaluate all cards
    var evalHands = [];
    for (i=0; i<hands.length; i++) {
        evalHands.push(PokerEvaluator.evalHand(hands[i]));
    }
    logd('Community cards : ' + this.communityCards[0] + ', ' + this.communityCards[1] + ', ' + this.communityCards[2] + ', ' + this.communityCards[3] + ', '  + this.communityCards[4]);
    // get highest value
    var highestVal = -9999;
    var highestIndex = -1;
    for (i=0; i<evalHands.length; i++) {
        logd('Player ' + this.players[i].name + ' : ' + this.players[i].firstCard + ', ' + this.players[i].secondCard + ' | strength : ' + evalHands[i].value + ' | ' + evalHands[i].handName);
        if (highestVal < evalHands[i].value) {
            highestVal = evalHands[i].value;
            highestIndex = i;
        }
    }
    logd('Player ' + this.players[highestIndex].name + ' wins with ' + evalHands[highestIndex].handName);
};

/**
 * Get the highest bet from all players
 * @returns {number} highestBet
 */
Game.prototype.getHighestBet = function() {
    var highestBet = -999;
    for(var i=0; i<this.players.length; i++) {
        if (highestBet < this.players[i].bet) {
            highestBet = this.players[i].bet;
        }
    }
    return highestBet;
};

/**
 * Collect all bets from players to the board's pot
 */
Game.prototype.gatherBets = function() {
    for(var i=0; i<this.players.length; i++) {
        this.pot += this.players[i].bet;
        this.players[i].bet = 0;
    }
    logd("Total Pot : " + this.pot)
};

/**
 * returns the player whose current turn it is
 * @returns {Player}
 */
Game.prototype.getCurrentPlayer = function() {
    return this.players[this.turnPos];
};

/**
 * Sets all players' hasActed to false
 */
Game.prototype.requestPlayerAction = function() {
    for (var i=0; i<this.players.length; i++) {
        if (!this.players[i].hasDone) {
            this.players[i].hasActed = false;
        }
    }
};

player.js

module.exports = Player;

var debug = true;
function logd(message) {
    if (debug) {
        console.log(message);
    }
}

function Player(options) {
    this.id = options.id;
    this.name = options.name;
    this.chips = options.chips;
    this.game = null;

    this.firstCard = {};
    this.secondCard = {};
    this.bet = 0;

    this.lastAction = "";
    this.hasActed = false;      // acted for one round (call/check/raise)
    this.hasDone = false;       // finish acted for one game (fold/allin)
}

/**
 * Folds the game
 */
Player.prototype.fold = function() {
    logd('Player ' + this.name + ' FOLD');

    this.lastAction = "fold";
    this.hasDone = true;

    this.game.incrementPlayerTurn();
    this.game.checkForNextRound();
};

/**
 * Puts all your chips to your bet
 */
Player.prototype.allin = function() {
    logd('Player ' + this.name + ' ALL-IN : ' + this.chips);

    this.lastAction = "allin";
    this.hasDone = true;

    this.addBet(this.chips);
    this.game.incrementPlayerTurn();
    this.game.checkForNextRound();
};

/**
 * Adds some chips to your bet
 * So that your bet is equal
 * With the highest bet in the table
 * If highest bet is 0, will do nothing
 */
Player.prototype.callOrCheck = function() {
    this.hasActed = true;

    var diff = this.game.getHighestBet() - this.bet;
    this.addBet(diff);

    if (diff > 0) {
        this.lastAction = "call";
        logd('Player ' + this.name + ' CALL : ' + diff);
    } else {
        this.lastAction = "check";
        logd('Player ' + this.name + ' CHECK');
    }
    this.game.incrementPlayerTurn();
    this.game.checkForNextRound();
};

/**
 * Raise your bet
 * If your bet is not the same with highest bet
 * Add to your bet altogether with difference
 * @param amount
 */
Player.prototype.raise = function(amount) {
    this.lastAction = "raise";

    var diff = this.game.getHighestBet() - this.bet;
    this.addBet(diff + amount);

    logd('Player ' + this.name + ' Raises : ' + (diff + amount));

    this.game.requestPlayerAction(); // other players must act
    this.hasActed = true;
    this.game.incrementPlayerTurn();
    this.game.checkForNextRound();
};

/**
 * Resets the player state
 */
Player.prototype.reset = function() {
    this.firstCard = {};
    this.secondCard = {};
    this.bet = 0;

    this.lastAction = "";
    this.hasActed = false;
    this.hasDone = false;
};

/**
 * Removes player's chip
 * Adds them to player's bet
 * @param amount
 */
Player.prototype.addBet = function(amount) {
    if (this.chips < amount) {
        return "error - not enough chips";
    }
    this.chips -= amount;
    this.bet += amount;
};

deck.js

module.exports = Deck;

function Deck() {
    this.suits = [ 's', 'h', 'd', 'c' ];
    this.ranks = [ '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A' ];
    this.cards = [];

    this.init();
    this.shuffle();
}

Deck.prototype.init = function() {
    var suitsLen = this.suits.length;
    var ranksLen = this.ranks.length;
    var i, j;

    for (i=0; i<suitsLen; i++) {
        for (j=0; j<ranksLen; j++) {
            this.cards.push( this.ranks[j] + this.suits[i] );
        }
    }
};

/**
 * Fisher-Yates Shuffle
 * https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
 */
Deck.prototype.shuffle = function() {
    var currentIndex = this.cards.length, temporaryValue, randomIndex ;

    // While there remain elements to shuffle...
    while (0 !== currentIndex) {

        // Pick a remaining element...
        randomIndex = Math.floor(Math.random() * currentIndex);
        currentIndex -= 1;

        // And swap it with the current element.
        temporaryValue = this.cards[currentIndex];
        this.cards[currentIndex] = this.cards[randomIndex];
        this.cards[randomIndex] = temporaryValue;
    }
};

Deck.prototype.drawCard = function () {
    return this.cards.pop();
};
\$\endgroup\$
  • 1
    \$\begingroup\$ I'm sorry if I'm missing something. I'm more of a beginner and was using your code as a reference for my own game. Nevertheless, I noticed that lastAction seems to be fairly pointless, and the way the showdown is structured seems to make it so that you can win even if you have folded. \$\endgroup\$ – Spencer Fleming Aug 25 '15 at 5:08
5
\$\begingroup\$

From a once over:

  • In index.js you keep using game.getCurrentPlayer()., since all those activities can only be done by the current player I feel the caller should just be able to just call game.callOrCheck(); and callOrCheck then figures out who the current player is.

  • You should use a common logger / output module instead of copy pasting in each module

  • This might be overkill, but read up on Model View Controller, you are mixing output and logic so much that you might regret it at some point

  • Consider using a library like lodash, it has a built-in function to shuffle arrays.

Other than that I like your code, I could not find much of any problem.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.