Presentation is loading. Please wait.

Presentation is loading. Please wait.

Building an Asynchronous Multiuser Web App for Fun... and Maybe Profit Luke Welling Laura Thomson

Similar presentations

Presentation on theme: "Building an Asynchronous Multiuser Web App for Fun... and Maybe Profit Luke Welling Laura Thomson"— Presentation transcript:

1 Building an Asynchronous Multiuser Web App for Fun... and Maybe Profit Luke Welling Laura Thomson

2 2 Introduction

3 3 Todays task: Texas Hold Em, OSCON Variant In this tutorial well step through designing and building a multiplayer game UI will be web based, which presents special challenges. The tools we will use are: HTML/CSS/JavaScript AJAX PHP MySQL

4 4 Speakers Luke Welling is a senior software engineer from Hitwise in Melbourne, Australia Laura Thomson is Director of Web Development at OmniTI in Columbia, Maryland We wrote PHP and MySQL Web Development (3/e, Sams Publishing, 2004) The most popular parts of the book are the projects (This talk is a work in progress of one of the projects from the 4/e.)

5 5 Motivation Todays talk should give you insight into: –Asynchronous web apps –Multiuser web apps and the challenges thereof –Using these technologies together But we were only joking about the profit part, sorry

6 6 Audience Youll get the most out of this talk if you: –Know PHP, but are not an expert –Know a little about the other technologies.

7 7 Introduction (You are here) The rules The goal The architecture The components Overview

8 8 The Goal

9 9 The rules

10 10 Texas Hold Em OSCON Variant: Basics Each player is dealt two pocket or hole cards. There are also 5 community cards. Each players hand consists of the best hand they can make out of those seven cards. (7C5, or 21 possible hands) Up to eight players

11 11 Sequence of Play Before any cards are dealt, first player posts a bet, called a small blind, and then second player posts a bigger bet, called a big blind Basically a tax on sitting at the table

12 12 Sequence of play - continued Each player is then dealt two cards A round of betting ensues Three community cards are dealt followed by a round of betting A fourth community card is dealt followed by a round of betting The fifth community card is dealt followed by a fourth and final round of betting

13 13 Betting Each player can either: –Call (sometimes referred to as see) – either match the existing bet, or go all in if they dont have enough money to match it –Raise – bet an increased amount –Fold – quit Betting rounds occur at several points throughout the game Each round may go through the players several times and ends when each player has either bet the same amount, folded, or gone all in

14 14 Ranking of hands Winners are then determined according to the standard ranking of poker hands: –Straight flush: Five cards in sequence and of the same suit. (Q J ) – Four of a kind: A hand with four cards of the same rank. ( ) – Full house: A hand with three of one rank and two of another. ( K K) – Flush: Five cards of the same suit. (K J 8 4 3) – Straight: Five cards in sequence. ( ) – Three of a kind: Three cards of the same rank. ( K 2) – Two pair: Two cards of one rank, two of another. ( A A 8 8 Q) – One pair: Two cards of the same rank. (9 9 A J 4) – High card: Also known as a "no pair" hand. The following example is considered "Ace high." ( A ) (Source:

15 15 Payout If you win you get the pot If theres a draw then its split between people that draw (Handling all in is more complicated and not on todays agenda)

16 16 The goal

17 17 The goal Build a system that allows people to play OSCON Texas Hold Em over the web without using Flash/Java applets etc, just using HTML and Ajax. Up to eight players at once No AI players (easier in many respects), AS instead On the way, we might discover why Flash and Java are more appropriate popular for this task

18 18 The architecture

19 19 Basic architecture UI Rules Engine Game Controller Database Ajax requests Game state changes, HTML Serialization Behaviors, Validity checking Renderer Initial HTML fragments

20 20 Overall architecture This is an MVC (Model – View – Controller) patterned architecture –UI + Renderer is the view –Game controller is the controller –Rules engine + DB is the model

21 21 UI UI is vanilla HTML + CSS + JavaScript Changes occur via Ajax updates on a per player basis UI sends requests to the controller –Create Game –Join Game –Start Game –Poll –Play (Call, Raise, Fold)

22 22 Controller Controller processes requests from the UI, and returns HTML for the portions of the UI that have changed. Controller processes the 4.2 different kinds of UI request

23 23 Create Game Triggers these events in the rule engine: –Creates the game object –Creates and shuffles the deck –Creates empty arrays and objects to hold the game objects –Serializes the Game for the first time

24 24 Game start Triggers these events in the rule engine: –Unserializes the empty Game –Adds dummy players –Deals with small and big blinds –Deals player cards –Serializes again so other players can load the same data

25 25 Poll UI gets state changes by polling the game controller Several different choices in how to implement game state updates in these kind of systems. Polling is the most common, and certainly the easiest to implement. Games are tracked via a gamestate (like a revision number). If a particular player is at revision x and the current gamestate is at y, we need to update their view to version y. This is the Periodic Refresh design pattern (

26 26 Clever Polling By tracking state number, we save processing and bandwidth You could refresh the whole screen every few seconds with plain HTML We can check every few seconds, but ignore most polls We only have to refresh if there has been a change We can just refresh the volatile parts We could fairly easily be a lot more clever and track at which state each UI item last changed, and only refresh the few that have changed recently

27 27 Plays There are three possible play actions: –Raise –Call (including all in) –Fold Check the validity of the play with the rules engine: whether a play is valid at any point depends where we are in the sequence of the game

28 28 Rules engine Rules engine is OO PHP, consisting of the following classes: –Game: representing the whole game –Card: a single card –CardCollection: a collection, strangely enough, of cards –Deck, a CardCollection, representing the deck to be used in the game, including card ordering for dealing purposes –Hands, are CardCollections, representing a players possible hands –HandCollection, includes features like sorting –CommunityCards, a CardCollection

29 29 Database The database contains the following tables: –players –games

30 30 mysql> describe players; | Field | Type | Null | Key | Default | Extra | | id | int(10) unsigned | | PRI | NULL |auto_increment| | password | varchar(40) | YES | | NULL | | | login | varchar(50) | YES | MUL | NULL | | | gameid | int(11) | YES | | 0 | | | name | varchar(50) | YES | | NULL | | rows in set (0.00 sec)

31 31 mysql> describe games; | Field | Type | Null | Key | Default | Extra | | id | int(10) unsigned | | PRI | NULL | auto_increment | | state | int(11) | | | 0 | | | data | blob | YES | | NULL | | rows in set (0.00 sec)

32 32 Database Serialization In general we are storing serialized objects from the Rules Engine as blobs, plus a game state version number Why not more granular data? For the purpose of this app we only need to access the above information. This is a pretty fast way to store and retrieve data for what we want to do. If we required any reporting or auditing functionality we would need to do more serious ORM

33 33 Issues and alternatives Sending changes: ghetto version control Server overhead / scalability Serialization Security

34 34 Version control About 75% of the way through implementation, complaints were heard: Arent we just re-implementing Subversion? There is a PHP extension for this: svn The difficulty would lie in writing a JavaScript client capable of doing svn update Nice idea though, and given more time and JS-Fu…

35 35 Scalability Polling is easy to implement. For one eight player game its pretty trivial for each browser to poll the server every 3 seconds If this became wildly successful this introduces a lot of load Not difficult load to manage: its just HTML, and small pieces at that. (Note were not resending the whole page, just portions of it) Some alternatives exist: –Keep connections open between requests –mod_pubsub to push from server to client –Implement the HTTP Streaming pattern (

36 36 Serialization ORM done in this app is extremely simplistic (using PHPs serialize() and unserialize() functions) Could be done in a more granular way using an ActiveRecord pattern implementation

37 37 Security This particular implementation is for fun. If we were playing for actual money we would need: –Better session management –A complete non-volatile audit trail of all plays (times, players, originating IPs) –SSL –More careful implementation of timing (or terms and conditions at least) to avoid litigation –To make it harder to read the HTML into a bot

38 38 The components

39 39 JavaScript // trigger a poll every three seconds, but don't // do one immediately // on page load as that would increase the risk of the // script being only partially loaded function periodicallyUpdate(first) { if(!first) { sendRequest("poll"); } setTimeout('periodicallyUpdate(0)',3000) }

40 40 Recipe 1 JavaScript file 1 CSS file 2 simple database tables Mostly PHP

41 41 Object Model HandCollection Controller Hand Game Player Renderer Deck CardCollection Card

42 42 // create a cross browser object to make our ajax requests through function createHttpRequestObject() { // use feature sniffing to find out what type of object to create var httpRequest; if (window.XMLHttpRequest) { // Mozilla, Safari,... httpRequest = new XMLHttpRequest(); } else if (window.ActiveXObject) { // IE httpRequest = new ActiveXObject("Microsoft.XMLHTTP"); } return httpRequest; } // make an instance of it var http = createHttpRequestObject();

43 43 //make an Ajax request function sendRequest(action) { // append what the client thinks the current state is to every request var state = parseInt(document.getElementById('clientState').value);'get', 'gameController.php?clientState='+state+'&action='+action); http.onreadystatechange = handleResponse; http.send(null); }

44 44 // when we get an answer we need to parse it and do something with the chunks // in this case, the pipe delimited format is very simple, and needs limited processing function handleResponse() { if(http.readyState == 4) { var response = http.responseText; var updates = new Array(); var count = 0; if(response.indexOf('|') != -1) { updates = response.split('|'); // make sure we have an even number of items, for pairs of id and text count = Math.floor((updates.length)/2)*2; for(var i=0; i

45 45 if('status' == updates[i]) { document.getElementById('statusBox').value += "\n"+updates[i+1]; } else if('clientState' == updates[i]) { document.getElementById(updates[i]).value = updates[i+1]; } else { document.getElementById(updates[i]).innerHTML = updates[i+1]; }

46 46 Data Format communityCards| |playerName| Not Playing This Game |playerCards| |playerText| Balance: $0.00 Stake: $0.00 Total Pot: $0.00 |player0Cards| |player0Text| 1: Empty Seat Balance: $0.00 Stake: $0.00 |player1Cards| |player1Text| 2: Empty Seat Balance: $0.00 Stake: $0.00 |player2Cards| |player2Text| 3: Empty Seat Balance: $0.00 Stake: $0.00 …

47 47 CSS Makes some things very easy // render a placeholder for a card so the //layout does not collapse public static function renderCardSpace() { return ' '; } // you see the backs of other players' cards public static function renderCardBack() { return ' '; }

48 48 … and some not so

49 49 Card CSS.card,.cardBack,.cardSpace { background-color: #fff; margin: 0.2em; float: left; border-color: #000; border-width:.05em; border-style: solid; position: relative; width: 11em; height: 14em; -moz-border-radius:0.75em; border-radius:0.75em; }

50 50 Same code – Same result public function renderCommunityCards($game = null) { if(is_a($game, 'Game')) { return $game->renderCommunityCards(); } else { // we are not in an active game return Card::renderCardSpace(). Card::renderCardSpace(). Card::renderCardSpace(); }

51 51 OO Delegation // render the collection, or with a count, blanks spaces to pad public function render($count=0) { if($count) { for($i=0; $i<$count; $i++) { if(is_a($this->cards[$i], 'Card')) { $return.= $this->cards[$i]->render(); } else { $return.= Card::renderCardSpace(); } else { foreach($this->cards as $card) { $return.= $card->render(); } return $return; }

52 52 public function render() { $colour = $this->getColour(); $entity = $this->getEntity(); $locations = $this->getLocations(); $return = " {$this->rank} $entity "; if('J'==$this->rank||'Q'==$this->rank||'K'==$this->rank) { $return.= " getLongRank().".gif' class = \"locFace\"> "; } else if ('A' == $this->rank) { $return.= " $entity

53 53 "; } else { foreach($locations as $location) { if('Face' != $location) { $return.= " {$entity} "; } $return.= " $entity $this->rank "; return $return; }

54 54 Player function load($id) { // the id will be passed in since it's been retrieved from the player's session // comes from authentication.php $this->db = new mysqli('localhost', 'poker', 'pokerpass', 'poker'); $id = intval($id); $query = "select name, gameid from players where id = $id"; $res = $this->db->query($query); if($res) { $name = mysqli_fetch_array($res); $name = $name[0]; $this->setName($name); $this->id = $id; } else { unset($this); }

55 55 More Player function setStatus($status) { switch($status) { case 'fold' : $this->cards = new CardCollection(); // fallthrough... case 'raise' : case 'call' : case 'allIn' : $this->status = $status; break; default : trigger_error("invalid status", E_USER_WARNING); }

56 56 Game::serialize() function serialize() { global $db; $db->autocommit(false); // serialize this object $query1 = "select state from games where id =". $this->id; $res1 = $db->query ($query1); $row = $res1->fetch_assoc(); $this->state = $row['state']; //update it in the object before serializing $this->state++; $query = "update games set data= '".mysqli_real_escape_string($db, serialize($this))."' where id = ".$this->id; $db->query($query); // update the state in the db too $query = "update games set state = (state+1) where id = ".$this->id; $db->query($query); $db->commit(); }

57 57 Game::unserialize() public static function unserialize($id) { global $db; $query = "select * from games where id = $id"; $result = $db->query($query); $row = $result->fetch_assoc(); $old = unserialize($row['data']); return $old; }

58 58 function bestHand($player) { if($player->getStatus()=='fold') { return null; } $stored = $this->bestHands->getHandByOwnerId($player- >getId()); if($stored) { return $stored; } // note this code is assuming 2 hole cards and 5 community cards // if you want to make another variant you will have to rewrite it $cards = new CardCollection();

59 59 $playerHands = new HandCollection(); $cards->add($this->communityCards->peek(0)); $cards->add($this->communityCards->peek(1)); $cards->add($this->communityCards->peek(2)); $cards->add($this->communityCards->peek(3)); $cards->add($this->communityCards->peek(4)); $cards->add($player->peekCard(0)); $cards->add($player->peekCard(1)); if($cards->count()!=7) { trigger_error("Cards missing", E_USER_WARNING); return null;

60 60 } // generate all possible hands of 5 from 7 cards for($i = 0; $i<6; $i++) { for($j = $i+1; $j<7; $j++) { $hand = array(); for($k=0; $k<7; $k++) { if($k!=$i&&$k!=$j) { $hand[]=$cards->peek($k); }

61 61 } $handObject = new Hand($hand, $player); $playerHands->add($handObject); } // 7C5 is 21, so if we are missing some possibilities, we have a problem if($playerHands->count()!=21) { trigger_error("Hands missing", E_USER_WARNING); } // get the best from the 21 possibles. return $playerHands->getBestHand(); }

62 62 Hand function handCompare($a,$b) { $a = $a->getNumericRank(); $b = $b->getNumericRank(); if($a==$b) { return 0; } else if ($a<$b) { return -1; } else // ($a>$b) { return 1; }

63 63 // calculate completely arbitrary numbers to rank hands // Yes, 15 is a magic number, // As Aces are being ranked as 14, rank/15 will give a number less than one public function getNumericRank() { if($this->isARoyalFlush()) { return 9; } if($this->isAStraightFlush()) { return 8+$this->getHighCard(1)->getNumericRank(); }

64 64 if($this->isAFourOfAKind()) { return 7+Card::getNumericRank($this- >isAFourOfAKind())/15; } if($this->isAFullHouse()) { return 6+ Card::getNumericRank($this- >isAThreeOfAKind())/15+ Card::getNumericRank($this- >getHighCard(4))/150; } if($this->isAFlush()) { return 5 + $this->getHighCard()->getNumericRank()/15 + $this->getHighCard(1)- >getNumericRank()/150 +

65 65 $this->getHighCard(2)- >getNumericRank()/ $this->getHighCard(3)- >getNumericRank()/ $this->getHighCard(4)- >getNumericRank()/150000; } if($this->isAStraight()) { return 4+$this->getHighCard()->getNumericRank()/15; } if($this->isAThreeOfAKind()) { return 3+Card::getNumericRank($this- >isAThreeOfAKind())/15; } if($this->isATwoPairs()) {

66 66 $pairRanks = $this->isATwoPairs(); $pairRanks[0] = Card::getNumericRank($pairRanks[0]); $pairRanks[1] = Card::getNumericRank($pairRanks[1]); // the hand was sorted, so the second rank is going to be the higher one return 2+$pairRanks[1]/15+($pairRanks[0]/150); } if($this->isAPair()) { return 1+Card::getNumericRank($this->isAPair())/15; } return $this->getHighCard()->getNumericRank()/15; }

67 67 Specific hand functions public function isAThreeOfAKind() { if( ($this->peek(0)->getRank() == $this->peek(1)->getRank() && $this->peek(1)->getRank() == $this->peek(2)->getRank() ) || ($this->peek(1)->getRank() == $this->peek(2)->getRank() && $this->peek(2)->getRank() == $this->peek(3)->getRank() ) || ($this->peek(2)->getRank() == $this->peek(3)->getRank() && $this->peek(3)->getRank() == $this->peek(4)->getRank() ) { // Three of a kind sorted into order will always have a card at position 2 return $this->peek(2)->getRank(); } else { return false; }

68 68 GameController /* This is the AJAX based controller that runs the game */ // get the overall action $action = $_GET['action']; $clientState = intval($_GET['clientState']); switch ($action) { case 'createGame': { $game = new Game(); $game->serialize(); break; }

69 69 GameController case 'startGame': { // temporary code until I write some join up code for players … $game->postSmallBlind(); $game->postBigBlind(); $game->dealPlayerCards(2); $game->serialize(); echo 'status|OK, game started|'; } else { echo 'status|Game already started|'; } break;

70 70 GameController case 'fold' : case 'call' : case 'raise' : { $game = Game::unserialize($gameId); $userId = intval($_SESSION['userId']); switch ($action) { case 'fold': // mark player as folded $game->fold($game->getPlayerById($userId)); break;

71 71 GameController case 'call' : $game->call($game->getPlayerById($userId)); //all the robo-players can call too, we want to go home one day $game->call($game->getPlayer(1)); $game->call($game->getPlayer(2)); $game->call($game->getPlayer(3)); $game->call($game->getPlayer(4)); break; case 'raise': $amount = floatval($_GET['$raiseAmount']); $game->raise($game->getPlayerById($userId), $raiseAmount); break; default: }

72 72 GameController // have we rolled over to a new betting round? { if($game->getBettingRound()>1 && $game->countCommunityCards()<3) { $game->dealCommunityCards(3); } else if($game->getBettingRound()>2 && $game- >countCommunityCards()<4) { $game->dealCommunityCards(1); } else if($game->getBettingRound()>3 && $game- >countCommunityCards()<5) { $game->dealCommunityCards(1); }

73 73 GameController else if($game->isGameOver()) { $winningHand = $game->getWinningHand(); $winningPlayer = $winningHand->getOwner(); $winningLocation = $game->getPlayerLocation($winningPlayer- >getId()); echo "status|Winner is ".$winningPlayer->getName()." with ". $winningHand->getRank().'|'; } $game->serialize(); } break; default: }

74 74 GameController if(!$game) { $game = Game::unserialize($gameId); } // update all - poll command, and after others if($clientState getState()) { echo 'communityCards|'.SectionRenderer::renderCommunityCards($game).'|'; // update player data echo 'playerName|'.SectionRenderer::renderPlayerName($game).'|'; echo 'playerCards|'.SectionRenderer::renderPlayerCards($game).'|'; echo 'playerText|'.SectionRenderer::renderPlayerText($game).'|';

75 75 GameController // update small player cards and text for($i = 0; $igetState().'|'; } else { // echo "status|no updates from state $clientState|"; }

76 76 Conclusions

77 77 Lessons learned JavaScript Libraries? FireBug is great Serialization in PHP is simple

78 78 Questions? Slides are online at

79 79 The J, Q, K Images Nicu Buculei Full sets of SVG cards Public Domain

80 80 A word from our sponsor… PHP Lightning talks: Book launch and signing at Powells onsite bookstore, Thursday

Download ppt "Building an Asynchronous Multiuser Web App for Fun... and Maybe Profit Luke Welling Laura Thomson"

Similar presentations

Ads by Google