Upload
colin-johnston
View
231
Download
2
Tags:
Embed Size (px)
Citation preview
Building an Asynchronous
Multiuser Web App for
Fun ... and Maybe Profit Luke [email protected] [email protected]
2
Introduction
3
Today’s task: Texas Hold ‘Em, OSCON Variant
• In this tutorial we’ll 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
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
Motivation
• Today’s 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
Audience
• You’ll get the most out of this talk if you:– Know PHP, but are not an expert
– Know a little about the other technologies.
7
• Introduction (You are here)
•The rules
•The goal
•The architecture
•The components
Overview
8
The Goal
9
The rules
10
Texas Hold ‘Em OSCON Variant:Basics
• Each player is dealt two pocket or hole cards.
• There are also 5 community cards.
• Each player’s hand consists of the best hand they can make out of those seven cards. (7C5, or 21 possible hands)
• Up to eight players
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
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
Betting• Each player can either:
– Call (sometimes referred to as “see”) – either match the existing bet, or go “all in” if they don’t 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
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♦ 10♦ 9♦ 8♦)– Four of a kind: A hand with four cards of the same rank. ( 4♣ 4♦ 4♥ 4♠ 9♥)– Full house: A hand with three of one rank and two of another. ( 8♣ 8♦ 8♠ K♥ K♠)– Flush: Five cards of the same suit. (K♠ J♠ 8♠ 4♠ 3♠)– Straight: Five cards in sequence. (7♦ 6♥ 5♠ 4♦ 3♦)– Three of a kind: Three cards of the same rank. ( 7♣ 7♥ 7♠ 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♦ 10♦ 9♠ 5♣ 4♣)(Source: http://en.wikipedia.org/wiki/Poker_hand)
15
Payout
• If you win you get the pot
• If there’s a draw then it’s split between people that draw
• (Handling “all in” is more complicated and not on today’s agenda)
16
The goal
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
The architecture
19
Basic architecture
UI
RulesEngine
Game Controller
Database
Ajax requests
Game statechanges, HTML
Serialization
Behaviors,Validity checking
RendererInitialHTML
fragments
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
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
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
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
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
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 (http://ajaxpatterns.org/Periodic_Refresh)
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
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
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 player’s possible
hands– HandCollection, includes features like sorting– CommunityCards, a CardCollection
29
Database
• The database contains the following tables:–players
–games
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 | |+----------+------------------+------+-----+---------+--------------+5 rows in set (0.00 sec)
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 | |
+-------+------------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
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
Issues and alternatives
• Sending changes: ghetto version control
• Server overhead / scalability
• Serialization
• Security
34
Version control
• About 75% of the way through implementation, complaints were heard: ‘Aren’t 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
Scalability
• Polling is easy to implement. • For one eight player game it’s 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: it’s just HTML, and small pieces at
that. (Note we’re 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
(http://ajaxpatterns.org/HTTP_Streaming)
36
Serialization
• ORM done in this app is extremely simplistic (using PHP’s serialize() and unserialize() functions)
• Could be done in a more granular way using an ActiveRecord pattern implementation
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
The components
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
Recipe
• 1 JavaScript file• 1 CSS file• 2 simple database tables• Mostly PHP
41
Object Model
HandCollection
Controller
Hand
Game
Player
Renderer
Deck
CardCollection
Card
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
//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);
http.open('get', 'gameController.php?clientState='+state+'&action='+action);
http.onreadystatechange = handleResponse;
http.send(null);
}
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<count; i+=2) {
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
Data Format
communityCards|<div class= "cardSpace"> </div><div class= "cardSpace"> </div><div class= "cardSpace"> </div><div class= "cardSpace"> </div><div class= "cardSpace"> </div>|playerName|<h2>Not Playing This Game</h2>|playerCards|<div class= "cardSpace"> </div><div class= "cardSpace"> </div>|playerText|<p class = "importantText">Balance: $0.00</p> <p class = "importantText">Stake: $0.00</p> <p class = "importantText">Total Pot: $0.00</p>|player0Cards|<div class= "cardSpace"> </div><div class= "cardSpace"> </div>|player0Text|<h2>1: Empty Seat</h2> <p>Balance: $0.00</p> <p>Stake: $0.00</p>|player1Cards|<div class= "cardSpace"> </div><div class= "cardSpace"> </div>|player1Text|<h2>2: Empty Seat</h2> <p>Balance: $0.00</p> <p>Stake: $0.00</p>|player2Cards|<div class= "cardSpace"> </div><div class= "cardSpace"> </div>|player2Text|<h2>3: Empty Seat</h2> <p>Balance: $0.00</p> <p>Stake: $0.00 …
47
CSS Makes some things very easy
// render a placeholder for a card so the
//layout does not collapse
public static function renderCardSpace()
{
return '<div class= "cardSpace"> </div>';
}
// you see the backs of other players' cards
public static function renderCardBack()
{
return '<div class="cardBack"> </div>';
}
48
… and some not so
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
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(). Card::renderCardSpace(). Card::renderCardSpace(); } }
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
public function render() { $colour = $this->getColour(); $entity = $this->getEntity(); $locations = $this->getLocations(); $return = "<div class=\"card $colour\"> <div class=\"rankArea $colour\"> <div class=\"rank $colour\">{$this->rank}</div> <div class=\"sideSuit $colour\">$entity</div> </div>"; if('J'==$this->rank||'Q'==$this->rank||'K'==$this->rank) { $return .= "<div class=\"locFace\"> <img src = 'images/".$this->getLongRank().".gif' class
= \"locFace\"> </div>"; } else if ('A' == $this->rank) { $return .= "<div class=\"locFace $colour\"> <span class=\"face $colour\">$entity</span>
53
</div>"; } else { foreach($locations as $location) { if('Face' != $location) { $return .= "<div class=\"loc{$location} $colour\"> <div class=\"suit $colour\">{$entity}</div> </div>"; } } } $return .= " <div class=\"bottomRankArea $colour\"> <div class=\"sideSuit $colour\">$entity</div> <div class=\"rank $colour\">$this->rank</div> </div> </div>"; return $return;• }
54
Playerfunction 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
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
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
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
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
$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
}
// 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
} $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
Handfunction 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
// 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
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
$this->getHighCard(2)->getNumericRank()/1500 +
$this->getHighCard(3)->getNumericRank()/15000 +
$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
$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
Specific hand functionspublic 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
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
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
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
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
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
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
GameController
if(!$game){ $game = Game::unserialize($gameId);}// update all - poll command, and after othersif($clientState<$game->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
GameController
// update small player cards and text for($i = 0; $i<MAX_PLAYERS; $i++) { echo 'player'.$i.'Cards|'.SectionRenderer::renderPlayerCards($game,
$i).'|'; echo 'player'.$i.'Text|'.SectionRenderer::renderPlayerText($game,
$i).'|'; } echo 'clientState|'.$game->getState().'|';}else{// echo "status|no updates from state $clientState|";}
76
Conclusions
77
Lessons learned
• JavaScript Libraries?• FireBug is great• Serialization in PHP is simple
78
Questions?
• Slides are online at
• http://lukewelling.com
• http://omniti.com/resources/talks
79
The J, Q, K Images
• Nicu Buculei
• http://www.nicubunu.ro/cards/
• Full sets of SVG cards
• Public Domain
80
A word from our sponsor…
• PHP Lightning talks: [email protected]
• Book launch and signing at Powell’s onsite bookstore, 12.30 Thursday