UNPKG

casino-server

Version:

An multi-rule scalable online poker game server powered by redis, node.js and socket.io

956 lines (775 loc) 20.9 kB
var Gamer = require('./gamer'), Room = require('./room'), Poker = require('./poker'), Holdem = require('./holdem_poker'); exports = module.exports = HoldemGame; var GAME_OVER = 0, SMALL_BLIND = 1, BIG_BLIND = 2, PREFLOP = 3, FLOP = 4, TURN = 5, RIVER = 6, SHOWDOWN = 7; var LIMIT = 0, POT_LIMIT = 1, NO_LIMIT = 2; var STATE = { 0: 'gameover', 1: 'ready', 2: 'binds', 3: 'preflop', 4: 'flop', 5: 'turn', 6: 'river', 7: 'showdown' }; function HoldemGame( casino, typeid, roomid, options ) { var defaults = { max_seats: 10, no_joker: true, no_color: [], no_number: [], ready_countdown: 10, turn_countdown: 10, limit_rule: 0, // 0: limit, 1: pot limit, 2: no limit limit: 100, // big blind limit_cap: 200, // -1, means no limit }; if(options && (typeof options === 'object')) { for(var i in options) defaults[i] = options[i]; } Room.call(this, casino, typeid, roomid, defaults); this.raise_per_round = (this.options.limit_rule === LIMIT) ? 3 : -1; this.raise_counter = 0; this.ready_gamers = 0; this.ready_countdown = -1; this.is_ingame = false; this.state = GAME_OVER; this.dealer_seat = 0; this.big_blind = this.options.limit; this.in_gamers = []; this.turn_countdown = -1; this.deal_order = []; this.cards = {}; this.shared_cards = []; this.chips = {}; this.pot_chips = []; this.pot = 0; this.max_chip = 0; this.last_raise = 0; this.no_raise_counter = 0; } HoldemGame.LIMIT = LIMIT; HoldemGame.POT_LIMIT = POT_LIMIT; HoldemGame.NO_LIMIT = NO_LIMIT; HoldemGame.prototype = Object.create(Room.prototype); HoldemGame.prototype.constructor = HoldemGame; HoldemGame.prototype.details = function() { var data = Room.prototype.details.call(this); data.cards = this.cards; data.chips = this.chips; data.dealer_seat = this.dealer_seat; data.shared_cards = this.shared_cards; data.pot = this.pot; data.pot_chips = this.pot_chips; data.max_chip = this.max_chip; data.last_raise = this.last_raise; return data; }; HoldemGame.prototype.tick = function() { Room.prototype.tick.call(this); var room = this; if(room.is_ingame) { var gamer = room.in_gamers[0]; if(room.turn_countdown > 0) { room.notifyAll('countdown', { seat: gamer.seat, sec: room.turn_countdown }); room.turn_countdown --; } else if(room.turn_countdown === 0) { // TODO: for test only room.gamerMoveTurn(true); //room.gamerGiveUp( gamer ); } else { // not started, just wait } } else { if(room.ready_countdown > 0) { room.notifyAll('countdown', { seat: -1, sec: room.ready_countdown }); room.ready_countdown --; } else if(room.ready_countdown === 0) { room.gameStart(); } else { // not ready, just wait } } }; HoldemGame.prototype.gameStart = function() { var room = this; var seats = room.seats; var gamers = room.gamers; room.is_ingame = true; room.ready_countdown = -1; room.big_blind = room.options.limit; room.shared_cards = []; room.cards = {}; room.chips = {}; room.pot_chips = []; room.pot = 0; room.max_chip = 0; room.last_raise = room.big_blind; room.raise_counter = 0; room.no_raise_counter = 0; var i, j, uid, gamer, first = room.dealer_seat; var in_gamers = room.in_gamers = []; for(i=first; i<seats.length; i++) { uid = seats[i]; if(uid) { gamer = gamers[ uid ]; if(gamer.is_ready) { in_gamers.push( gamer ); } } } for(i=0; i<first; i++) { uid = seats[i]; if(uid) { gamer = gamers[ uid ]; if(gamer.is_ready) { in_gamers.push( gamer ); } } } var deal_order = room.deal_order = []; var in_seats = []; for(j=0; j<in_gamers.length; j++) { gamer = in_gamers[j]; room.ready_gamers --; gamer.is_ready = false; gamer.is_ingame = true; gamer.is_cardseen = false; gamer.is_allin = false; gamer.cards = []; gamer.chips = 0; gamer.prize = 0; deal_order.push( gamer ); in_seats.push( gamer.seat ); } room.ingamers_count = in_gamers.length; if(in_gamers.length > 2) in_gamers.push( in_gamers.shift() ); // else: 1 vs 1 -- heads up // small blind var small = in_gamers.shift(); in_gamers.push( small ); gamer = small; var n = Math.round( room.big_blind * 0.5 ); gamer.profile.coins -= n; gamer.chips += n; room.chips[ gamer.seat ] = gamer.chips; room.pot_chips.push(n); room.pot += n; room.max_chip = n; // big blind var big = in_gamers.shift(); in_gamers.push( big ); gamer = big; n = room.big_blind; gamer.profile.coins -= n; gamer.chips += n; room.chips[ gamer.seat ] = gamer.chips; room.pot_chips.push(n); room.pot += n; room.max_chip = n; room.notifyAll('gamestart', { room: room.details(), seats: in_seats }); // deal hole cards var fullcards = room.fullcards = Poker.newSet(room.options); var roomcards = room.cards = {}; var deals = []; for(i=0; i<deal_order.length; i++) { gamer = deal_order[i]; if(gamer.is_ingame) { gamer.cards = Poker.sortByNumber( Poker.draw(fullcards, 2) ); roomcards[ gamer.seat ] = [0,0]; deals.push([ gamer.seat, [0,0] ]); } } room.notifyAll('deal', { deals: deals, delay: 3 }); for(i=0; i<deal_order.length; i++) { gamer = deal_order[i]; if(gamer.is_ingame) { room.notify(gamer.uid, 'seecard', { seat: gamer.seat, uid: gamer.uid, cards: gamer.cards }); } } setTimeout(function(){ room.state = PREFLOP; room.gamerMoveTurn(false); }, 3000); }; HoldemGame.prototype.dealSharedCards = function(n) { var room = this; var cards = Poker.draw(room.fullcards, n); room.shared_cards = Poker.merge(room.shared_cards, cards); var deals = []; deals.push([-1, cards]); room.notifyAll('deal', { deals: deals, delay: 1 }); }; HoldemGame.prototype.gameOver = function() { var room = this; var in_gamers = room.in_gamers, i, gamer, item, scorelist = []; for(i=0; i<in_gamers.length; i++) { gamer = in_gamers[i]; gamer.is_ingame = false; if(gamer.prize > 0) { gamer.profile.exp += 2; gamer.profile.score ++; gamer.profile.coins += gamer.prize; gamer.saveData(); } item = gamer.getProfile(); item.seat = gamer.seat; item.cards = gamer.cards; item.chips = gamer.chips; item.prize = gamer.prize; scorelist.push(item); } room.notifyAll('gameover', scorelist); room.notifyAll('prompt', { fold: null, check: null, call: null, raise: null, all_in: null, ready: true }); room.is_ingame = false; room.turn_countdown = -1; room.in_gamers = []; room.ingamers_count = 0; // for next round, move deal seat to next room.dealer_seat = room.deal_order[0].seat; room.deal_order = []; room.cards = {}; room.chips = {}; room.pot_chips = []; room.pot = 0; room.max_chip = 0; room.last_raise = 0; }; HoldemGame.prototype.cmdsForGamer = function(gamer) { var room = this; var limit_rule = room.options.limit_rule; var cmds = {}; switch(room.state) { case PREFLOP: case FLOP: case TURN: case RIVER: cmds.fold = true; var call_chip = (room.max_chip - gamer.chips); var raise_max = gamer.profile.coins - call_chip; if(call_chip > 0) { if(raise_max >= 0) cmds.call = true; } else { cmds.check = true; } if(raise_max >= room.last_raise) { if(limit_rule === POT_LIMIT) { raise_max = Math.min(raise_max, room.pot + call_chip); cmds.raise = 'range,' + room.last_raise + ',' + raise_max; } else if(limit_rule === NO_LIMIT) { cmds.raise = 'range,' + room.last_raise + ',' + raise_max; } else if(limit_rule === LIMIT) { var allow_raise = ((room.raise_per_round > 0) && (room.raise_counter < room.raise_per_round)); switch(room.state) { case PREFLOP: case FLOP: if(allow_raise) { cmds.raise = [ room.big_blind ]; // small bet } break; case TURN: case RIVER: if(allow_raise) { cmds.raise = [ room.big_blind * 2 ]; // big bet } break; } } } if(limit_rule === NO_LIMIT) { if(gamer.profile.coins > 0) cmds.all_in = true; } break; } return cmds; }; HoldemGame.prototype.moveTurnToNext = function() { var room = this; var in_gamers = room.in_gamers; var last = in_gamers[0], next; room.notify(last.uid, 'prompt', { fold: null, check: null, call: null, raise: null, all_in: null }); do { in_gamers.push( in_gamers.shift() ); next = in_gamers[0]; if(next.seat === room.first_turn) room.round_counter ++; // to avoid dead loop if(next.seat === last.seat) break; // we find the next one in game if(next.is_ingame) { if(next.is_allin) { room.no_raise_counter ++; } else { break; } } } while(true); }; HoldemGame.prototype.gamerMoveTurn = function(move) { var room = this; var in_gamers = room.in_gamers, deal_order = room.deal_order; if(move) room.moveTurnToNext(); var deal_card = false; if(room.no_raise_counter === room.ingamers_count) { room.state ++; switch(room.state) { case FLOP: room.dealSharedCards(3); deal_card = true; break; case TURN: room.dealSharedCards(1); deal_card = true; break; case RIVER: room.dealSharedCards(1); deal_card = true; break; } room.no_raise_counter = 0; room.raise_counter = 0; // after dealing, we start bet next round from the first for(var i=0; i<deal_order.length; i++) { in_gamers[i] = deal_order[i]; } room.moveTurnToNext(); } var gamer = in_gamers[0]; room.turn_countdown = room.options.turn_countdown; room.notifyAll('moveturn', { seat: gamer.seat, uid: gamer.uid, countdown: room.turn_countdown }); if(deal_card) { setTimeout(function(){ room.notify(gamer.uid, 'prompt', room.cmdsForGamer(gamer) ); }, 1000); } else { if(room.state < SHOWDOWN) { room.notify(gamer.uid, 'prompt', room.cmdsForGamer(gamer) ); } else { room.gamerShowDown(); } } }; HoldemGame.prototype.gamerGiveUp = function( gamer ) { var room = this; var in_gamers = room.in_gamers; room.notifyAll('fold', { seat: gamer.seat, uid: gamer.uid }); gamer.is_ingame = false; room.ingamers_count --; gamer.profile.exp ++; //gamer.profile.score --; gamer.saveData(); room.notify(gamer.uid, 'prompt', { fold: null, check: null, call: null, raise: null, all_in: null }); if(room.ingamers_count === 1) { room.simpleWin(); } else { var is_myturn = (gamer.seat === in_gamers[0].seat); if(is_myturn) room.gamerMoveTurn(true); } }; HoldemGame.prototype.simpleWin = function() { var room = this, in_gamers = this.in_gamers; var prize = room.pot; // TODO: /* var rake = Math.round( room.pot * room.options.rake_percent ); if(room.options.rake_pot > 0) { if(room.pot < room.options.rake_pot) rake = 0; } if(room.options.rake_cap > 0) { if(rake > room.options.rake_cap) rake = room.options.rake_cap; } var prize = room.pot - rake; */ for(var i=0; i<in_gamers.length; i++) { var gamer = in_gamers[i]; gamer.prize = 0; if(gamer.is_ingame) { gamer.prize = prize; gamer.is_ingame = false; room.ingamers_count --; break; } } room.gameOver(); }; HoldemGame.prototype.gamerShowDown = function() { var room = this; var in_gamers = room.in_gamers, finals = [], gamers_bychips = []; var i, gamer, maxFive, someone_allin = false; for(i=0; i<in_gamers.length; i++) { gamer = in_gamers[i]; gamers_bychips.push( gamer ); if(! gamer.is_ingame) continue; if(gamer.is_allin) someone_allin = true; maxFive = Holdem.maxFive(gamer.cards, room.shared_cards); if(maxFive) { gamer.maxFiveRank = Holdem.rank( maxFive ); finals.push( gamer ); } } finals.sort( function(a,b){ return b.maxFiveRank - a.maxFiveRank; } ); if(someone_allin) { // if someone allin, the pot distribution will be complex /* * 当有一或多个牌手全押时,德州扑克的彩池分配较为复杂,超过牌手押注金额的部份将会形成一或多个边池。 * 牌手参与投注该彩池才有机会于该彩池胜出分配奖金。 * * 当一局结束而且有“全押”的牌手赢牌时,该牌手有参与投注的主池边池奖金均归该牌手。 * 而其他边池由参与该边池投注里,持有最大牌面的牌手赢得。 * 在几个牌手全押形成多个边池时,依全押的顺序分配给各边池中最佳牌面的牌手。 * 无人跟注的边池(仅有一位牌手下注,剩下其他牌手都盖牌)将会直接赢得该边池。 * * 彩池分配范例: * * 例如ABCDEF六名牌手参与牌局,F于中途盖牌退出,最终A全押投入$50,B全押投入$250,C全押投入$350, * DE各投入$800,F投入$500,此时总彩池大小为$2750,形成了一个主池为50*6=$300, * 边池各为(250-50)*5=$1000,(350-250)*4=$400,(500-350)*3=$450,(800-500)*2=$600, * 若最终组成牌面大小为F>A>B>D>E>C,但F已盖牌不能分配任何彩池,则此局主池即为A于此局赢得的筹码($300), * B可赢得第一个边池($1000),D参与至最后一个边池,且牌面胜过参与第二、第三及第四边池的所有牌手, * 因此可赢得剩下所有的边池(400+450+600=$1450)。 */ gamers_bychips.sort( function(a,b) { return a.chips - b.chips; } ); } else { // only keep the largest one, may be one, two, or more same big /* * 当没有牌手全押(all-in)时,彩池由未盖牌的牌手中牌型最大的者独得。 * 如多于一名牌手拥有最大的手牌,彩池会由他们平等均分。 * 不能平分的零头数筹码由发牌者后依顺时针方向,尚未盖牌的第一个牌手获得(即位置相对最不利者)。 * * 举例来说: * * 有ABCDE依顺时钟方向入座,A为本局发牌者,最小面额筹码为$10,所有牌手皆未盖牌至斗牌, * 最终由CDE胜出平分本局彩池$1000时,则DE各分到$330,而多出的$10将分配给最靠近A的赢家C,C于本局可分到$340。 */ for(i=0; i<finals.length-1; i++) { if(finals[i].maxFiveRank > finals[i+1].maxFiveRank) { var losers = finals.splice(i+1, Number.MAX_VALUE); while(losers.length > 0) { var loser = losers.shift(); loser.profile.exp ++; loser.saveData(); } break; } } var prize = room.pot; var n = finals.length; if(n > 1) { var average = Math.floor( prize / n ); for(i=0; i<finals.length; i++) { finals[i].prize = average; } var odd = prize % n; if(odd > 0) { // find the nearest winner after dealer seat finals.sort( function(a,b){ return a.seat - b.seat; } ); var first_seat = finals[0].seat, dealer_seat = room.dealer_seat; do { gamer = finals.pop(); if(gamer.seat > dealer_seat) { finals.unshift( gamer ); } else { finals.push( gamer ); break; } } while (gamer.seat !== first_seat); finals[0].prize += odd; } } else { finals[0].prize = prize; } } room.gameOver(); }; HoldemGame.prototype.onGamer_ready = function(req, reply) { var room = this; var uid = req.uid; var gamer = room.gamers[ uid ]; if(gamer.seat < 0) { reply(400, 'you must take a seat to play'); return; } if(room.is_ingame) { reply(400, 'game already started, wait next round'); return; } if(gamer.is_ingame) { reply(400, 'already in game'); return; } if(gamer.is_ready) { reply(400, 'already ready'); return; } gamer.is_ready = true; room.ready_gamers ++; room.notifyAll('ready', { uid: uid, where: gamer.seat }); if(room.ready_gamers >= 2) { if(room.ready_gamers === room.seats_taken) { room.gameStart(); } else if(room.ready_gamers === 2) { room.ready_countdown = room.options.ready_countdown; room.notifyAll('countdown', { seat: -1, sec: room.ready_countdown }); } } reply(0, { cmds: { ready: null } }); }; HoldemGame.prototype.onGamer_takeseat = function(req, reply) { var room = this; var uid = req.uid; var gamer = room.gamers[ uid ]; if(gamer.profile.coins < room.options.limit) { reply(400, 'no enough coins, need at least: ' + room.options.limit); return; } Room.prototype.onGamer_takeseat.call(this, req, function(err,ret){ if(! err) { if(! ret.cmds) ret.cmds = {}; ret.cmds.ready = true; } reply(err, ret); }); }; HoldemGame.prototype.onGamer_unseat = function(req, reply) { var room = this; var uid = req.uid; var gamer = room.gamers[ uid ]; var cmds = {}; if(gamer.is_ingame) { room.onGamer_fold(req, function(e,r){ if((!e) && r.cmds) { for(var i in r.cmds) cmds[i] = r.cmds[i]; } }); } if(gamer.is_ready) { gamer.is_ready = false; room.ready_gamers --; } cmds.ready = null; Room.prototype.onGamer_unseat.call(this, req, function(e,r){ if((!e) && r.cmds) { for(var i in r.cmds) cmds[i] = r.cmds[i]; } }); reply(0, { cmds: cmds }); }; HoldemGame.prototype.onGamer_fold = function(req, reply) { var room = this, uid = req.uid; var gamers = room.gamers; var gamer = gamers[ uid ]; if(! gamer.is_ingame) { reply(400, 'no in game'); return; } room.gamerGiveUp( gamer ); reply(0, {}); }; HoldemGame.prototype.onGamer_check = function(req, reply) { var room = this, uid = req.uid; var gamers = room.gamers; var gamer = gamers[ uid ]; if(! gamer.is_ingame) { reply(400, 'no in game'); return; } var call_chip = room.max_chip - gamer.chips; if(call_chip > 0) { reply(400, 'you should call'); return; } room.no_raise_counter ++; room.notifyAll('check', { seat: gamer.seat, uid: gamer.uid, call: 0, raise: 0 }); reply(0, {}); room.gamerMoveTurn(true); }; HoldemGame.prototype.onGamer_call = function(req, reply) { var room = this, uid = req.uid; var gamers = room.gamers; var gamer = gamers[ uid ]; if(! gamer.is_ingame) { reply(400, 'no in game'); return; } var call_chip = room.max_chip - gamer.chips; var n = call_chip; if(n > gamer.profile.coins) { reply(400, 'no enough coins for call: ' + n); return; } room.no_raise_counter ++; gamer.profile.coins -= n; gamer.chips += n; room.chips[ gamer.seat ] = gamer.chips; room.pot_chips.push(n); room.pot += n; room.notifyAll('call', { seat: gamer.seat, uid: gamer.uid, call: n, raise: 0 }); reply(0, {}); room.gamerMoveTurn(true); }; HoldemGame.prototype.onGamer_raise = function(req, reply) { var room = this, uid = req.uid; var gamers = room.gamers; var gamer = gamers[ uid ]; if(! gamer.is_ingame) { reply(400, 'no in game'); return; } var raise = parseInt( req.args ); if(isNaN(raise) || (raise < room.last_raise)) { reply(400, 'invalid raise: ' + raise); return; } var call_chip = room.max_chip - gamer.chips; var n = call_chip + raise; if(n > gamer.profile.coins) { reply(400, 'no enough coins, need: ' + n); return; } if(room.raise_per_round > 0) { if(room.raise_counter >= room.raise_per_round) { reply(400, 'no more raise this round'); return; } room.raise_counter ++; } room.no_raise_counter = 1; room.notifyAll('raise', { seat: gamer.seat, uid: gamer.uid, call: call_chip, raise: raise }); gamer.profile.coins -= n; gamer.chips += n; room.chips[ gamer.seat ] = gamer.chips; room.pot_chips.push(n); room.pot += n; room.max_chip = Math.max(room.max_chip, gamer.chips); room.last_raise = raise; reply(0, {}); room.gamerMoveTurn(true); }; HoldemGame.prototype.onGamer_all_in = function(req, reply) { var room = this, uid = req.uid; var gamers = room.gamers; var gamer = gamers[ uid ]; if(! gamer.is_ingame) { reply(400, 'no in game'); return; } var n = gamer.profile.coins; gamer.is_allin = true; gamer.profile.coins -= n; gamer.chips += n; room.chips[ gamer.seat ] = gamer.chips; room.pot_chips.push(n); room.pot += n; room.max_chip = Math.max(room.max_chip, gamer.chips); room.notifyAll('all_in', { seat: gamer.seat, uid: gamer.uid, call: 0, raise: n }); reply(0, {}); room.gamerMoveTurn(true); }; HoldemGame.prototype.onGamer_relogin = function(req, reply) { Room.prototype.onGamer_relogin.call(this, req, reply); var room = this, uid = req.uid; var gamer = room.gamers[ uid ]; if(gamer.seat >= 0) { room.notify(uid, 'seecard', { seat: gamer.seat, uid: uid, cards: gamer.cards }); var is_myturn = false; var cmds = { ready: true, fold: null }; if(gamer.is_ready || gamer.is_ingame) cmds.ready = null; if(gamer.is_ingame) { cmds.fold = true; var next = room.in_gamers[0]; is_myturn = (next.seat === gamer.seat); } room.notify(uid, 'prompt', cmds); if(is_myturn) { room.gamerMoveTurn(false); } } }; HoldemGame.prototype.close = function() { var room = this; if(room.is_ingame) { room.gameOver(); } Room.prototype.close.call(this); };