Blackjack

Tellme Blackjack demonstrates how easily one can build great phone applications by combining the capabilities of VoiceXML and Javascript. Since the purpose of the VoiceXML language is to specify constructs for creating voice interfaces analogous to how HTML is intended for creating visual interfaces, it lacks many of the built-in objects to which Web developers are accustomed. Fortunately, in the same way that Javascript is used to create sophisticated HTML-based Web applications, JavaScript can be used on the Tellme platform to build sophisticated VoiceXML-based Voice applications. Any developer who knows how to write Javascript code for the Web can write Javascript code for the phone. The syntax and behavior of the language is exactly the same.

Try It!
  1. Set your Application URL to http://studio.247-inc.net/library2/code/ex-106/index.vxml
  2. Call the number shown in VXML Tools page
  3. Sign in, and play!

The game logic is largely encapsulated within a single Blackjack object and multiple Card objects. These objects are UI-independent and rely upon the application that utilizes them to provide an appropriate user interface. Because the Blackjack and Card classes are contained in an external file and sourced into the voice application via the script element, they can be re-used easily by other Web-based applications.

The application supports different voice "skins." By saying "change dealer" during game play, the user can toggle between the "Secret Agent" voice and the "Country Dealer" voice. Implementation of different skins is achieved by storing a set of identically named audio files in two separate directories. In this application, the Secret Agent audio files are stored in the directory "set2", and the Country Dealer audio files are stored in the directory "set4". All audio files are referenced by concatenating a document-scoped variable gsSkinPath containing the path to the current audio files with the physical filename. When the user says "change dealer," the application toggles the value of gsSkinPath.

The application consists of the following ten forms:

PlayBlackJack Welcomes the user to the game and provides instructions.
EvalDeal Shuffles the deck and deals the initial cards to the user and dealer.
PlayDeal Plays back the value of the cards to the user.
ChangeDealer Switches dealers.
AskHit Prompts the user to take another card
EvalHit Deals the user another card and calculates the new total.
PlayNewCard Plays back the value of the user's new card and plays the user's new total.
TestOver Determines the state of the game given the user and dealer totals.
EvalDone Wraps up the game playing back the final results.
PlayDone Asks the user to play again.

This example takes advantage of variable scoping to share application state among the various dialogs. For example, the variable gsSkinPath is defined at document scope and controls the path to the set of recorded audio files used to represent the dealer's voice.

The property element is used to tune the behavior of the interactive dialogs in the application.

Links are defined at the document level to handle "new dealer", "repeat", and "help" requests in the interactive dialogs of the application in a consistent manner.

The VoiceXML code for this example follows:

<?xml version="1.0"?>
<!--
Tellme Blackjack (Tellme Studio Code Example 106)
Copyright (C) 1999-2002 Tellme Networks, Inc. All Rights Reserved.

THIS CODE IS MADE AVAILABLE SOLELY ON AN "AS IS" BASIS, WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION,
WARRANTIES THAT THE CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A
PARTICULAR PURPOSE OR NON-INFRINGING.
-->
<vxml version="2.0">

<!-- common field tuning parameters -->
<property name="confidencelevel" value=".30"/>
<property name="timeout" value="5s"/>
<property name="tellme.endseconds" value="0.25"/>
<property name="tellme.pruning" value="500"/>
<property name="tellme.bargeinlevel" value="12"/>
<property name="tellme.magicword" value="true"/>

<!-- data/state to be shared across dialogs -->
<var name="gsSkinPath" expr="'set2'"/> <!-- path to the current audio 'skin' -->
<var name="gsNextDialog"/> <!-- navigation state tracking -->
<var name="gsPlayState"/> <!-- game state tracking -->
<var name="gsGameResult" expr="0"/> <!-- win/lose -->
<var name="gsDealerTotal" /> <!-- number of points the dealer ended up with -->
<var name="gsPlayerNewCard"/> <!-- latest card dealt to the player -->
<var name="gsPlayerTotal"/> <!-- player's current total -->

<var name="gsPlayerCard1"/> <!-- player's 1st card -->
<var name="gsPlayerCard2"/> <!-- player's 2nd card -->
<var name="gsDealerCardShowing"/> <!-- dealer's exposed card -->

<!-- 
when the user requests a new dealer, 
stop everything and jump to the change_dealer dialog.
-->
<link event="tellme.blackjack.newdealer">
<grammar mode="voice"
         root="gsl_top"
         version="1.0" tag-format="semantics/1.0"
         xml:lang="en-US">
 <rule id="gsl_top" scope="public">
  <one-of>
   <item>
    new dealer
   </item>
   <item>
    <item>
     change dealer
    </item>
   </item>
  </one-of>
 </rule>

</grammar>
<grammar mode="dtmf"
         root="gsl_top"
         tag-format="semantics/1.0"
         version="1.0">
 <rule id="gsl_top" scope="public">
  <item>
   3
  </item>
 </rule>

</grammar>

</link>

<catch event="tellme.blackjack.newdealer">
   <goto next="#ChangeDealer"/>
</catch>

<!-- 
when the user says repeat, replay the current dialog
-->
<link event="tellme.blackjack.repeat">
<grammar mode="voice"
         root="gsl_top"
         version="1.0" tag-format="semantics/1.0"
         xml:lang="en-US"
         >
 <rule id="gsl_top" scope="public">
  <one-of>
   <item>
    repeat
   </item>
  </one-of>
 </rule>

</grammar>

</link>

<catch event="tellme.blackjack.repeat">
   <goto expr="gsPlayState"/>
</catch>

<link event="help">
<grammar mode="voice"
         root="gsl_top"
         version="1.0" tag-format="semantics/1.0"
         xml:lang="en-US"
         >
 <rule id="gsl_top" scope="public">
  <item>
   help
  </item>
 </rule>

</grammar>
<grammar mode="dtmf"
         root="gsl_top"
         tag-format="semantics/1.0"
         version="1.0"
         >
 <rule id="gsl_top" scope="public">
  <item>
   0
  </item>
 </rule>

</grammar>

</link>

<!-- source in the Blackjack class -->
<script src="bj.js"/>
<script><![CDATA[
   // Instantiate a Blackjack object once, and reuse it
   var oBJ = new Blackjack();
]]></script>
   

<form id="PlayBlackJack">
   <block>      
      <audio expr="gsSkinPath + '/welcome01.wav'"/>
      <audio expr="gsSkinPath + '/to_exit.wav'"/>
      <audio expr="gsSkinPath + '/play01.wav'"/>
      <goto next="#EvalDeal"/>
   </block>
</form>

<!-- 
Initialize the player and dealer hands and the array that tracks dealt cards. 
Then deal cards to the caller and dealer. 
-->
<form id="EvalDeal">
   <block>
    <script>
    <![CDATA[

      // Initialize the Blackjack object
      oBJ.Init();
                
      var oPlayerCard1 = oBJ.Hit("caller");
      var oPlayerCard2 = oBJ.Hit("caller");
      gsPlayerCard1 = oPlayerCard1.GetTypeSuit();
      gsPlayerCard2 = oPlayerCard2.GetTypeSuit();
      gsPlayerTotal = oBJ.GetTotalOf("caller").toString();

      // According to http://www.gamblingtimes.com/gtbasbj.html, dealer gets TWO cards initially
      oBJ.Hit("dealer");         
      var oDealerCard2 = oBJ.Hit("dealer");         
      gsDealerCardShowing = oDealerCard2.GetTypeSuit();

      gsNextDialog = "#AskHit";
      gsPlayState = "#PlayDeal";
            
    ]]>
    </script>
   <goto next="#PlayDeal"/>
   </block>
</form>
   
<form id="PlayDeal">
   <block>
      <log>pc1= <value expr="gsPlayerCard1"/>, pc2= <value expr="gsPlayerCard2"/>, dc1= <value expr="gsDealerCardShowing"/></log>
      <audio expr="gsSkinPath + '/shuffle.wav'"/>
      <audio expr="gsSkinPath + '/deal01.wav'"/>
      <audio expr="gsSkinPath + '/' + gsPlayerCard1 + '.wav'"/>
      <audio expr="gsSkinPath + '/and.wav'"/>
      <audio expr="gsSkinPath + '/' + gsPlayerCard2 + '.wav'"/>
      <audio expr="gsSkinPath + '/total01.wav'"/>
      <audio expr="gsSkinPath + '/' + gsPlayerTotal + '.wav'"/>
      <audio expr="gsSkinPath + '/showing.wav'"/>
      <audio expr="gsSkinPath + '/' + gsDealerCardShowing + '.wav'"/>
      <goto expr="gsNextDialog"/>
   </block>
</form>

<!-- swap skins, and start over -->
<form id="ChangeDealer">
   <block>
    <script>
    <![CDATA[
      gsSkinPath = ((gsSkinPath == "set2") ? "set4" : "set2"); 
    ]]>         
    </script>
    <goto next="#PlayBlackJack"/>
   </block>
</form>

<!-- Ask the user if they want another card -->
<form id="AskHit">

   <field name="askhit">
      <prompt>
        <audio expr="gsSkinPath + '/another01.wav'"/>
      </prompt>

<grammar mode="voice"
         root="gsl_top"
         version="1.0" tag-format="semantics/1.0"
         xml:lang="en-US"
         >
 <rule id="gsl_top" scope="public">
  <one-of>
   <item>
    <item>
     yes
    </item>
    <tag>out = "yes";</tag>
   </item>
   <item>
    <item>
     no
    </item>
    <tag>out = "no";</tag>
   </item>
  </one-of>
 </rule>

</grammar>
<grammar mode="dtmf"
         root="gsl_top"
         tag-format="semantics/1.0"
         version="1.0"
         >
 <rule id="gsl_top" scope="public">
  <one-of>
   <item>
    <item>
     1
    </item>
    <tag>out = "yes";</tag>
   </item>
   <item>
    <item>
     2
    </item>
    <tag>out = "no";</tag>
   </item>
  </one-of>
 </rule>

</grammar>



      <noinput>
        <goto expr="gsPlayState"/>
      </noinput>
         
      <nomatch>
        <reprompt/>
      </nomatch>
         
      <help>
        <audio expr="gsSkinPath + '/help-se.wav'"/>
        <audio expr="gsSkinPath + '/to_exit.wav'"/>
        <reprompt/>
      </help>
         
      <catch event="nomatch noinput">
        <goto expr="gsPlayState"/>
      </catch>

      <filled>
        <if cond="'yes'==askhit">
          <goto next="#EvalHit"/>
        <elseif cond="'no'==askhit"/>
          <goto next="#EvalDone"/>
        </if>
      </filled>

   </field>
</form>

<!-- deal a card to the user -->
<form id="EvalHit">
   <block>
    <script>
    <![CDATA[
      var oPlayerNewCard = oBJ.Hit("caller");
      gsPlayerNewCard = oPlayerNewCard.GetTypeSuit();
      gsPlayerTotal = oBJ.GetTotalOf("caller").toString();
    ]]>
    </script>
    <goto next="#PlayNewCard"/>
   </block>
</form>

<!-- play back the player's new card and total hand value -->
<form id="PlayNewCard">
   <block>
      <audio expr="gsSkinPath + '/deal02.wav'"/>
      <audio expr="gsSkinPath + '/' + gsPlayerNewCard + '.wav'"/>
      <audio expr="gsSkinPath + '/total01.wav'"/>
      <audio expr="gsSkinPath + '/' + gsPlayerTotal + '.wav'"/>
      <goto next="#TestOver"/>
   </block>
</form>

<!-- has the player's hand gone over 21? -->
<form id="TestOver">
   <block>
    <script>
    <![CDATA[
    // if caller exceeded 21. Pick a random "you lose" audio file.
    if(oBJ.GetTotalOf("caller") > 21)
    {
      /* 
      There are 4 winner and 4 loser audio files.
      Use the JS Math object to generate a random number in the range of 1..4,
      and map the result to an audio file of the form lose0x.wav where x = [1..4]
     */
      var iRandResult = Math.floor(Math.random()*4) +1;
      gsDealerTotal = oBJ.GetTotalOf("dealer").toString();
      gsGameResult = "lose0" + iRandResult.toString();
      gsNextDialog = "#PlayDone";
    }
    else
    {
      gsPlayState = "#PlayNewCard";
    }
    ]]>
    </script>
   <goto expr="gsNextDialog"/>
   </block>
</form>

<!-- finish up the current hand, and determine the results -->
<form id="EvalDone">
   <block>
    <script>
    <![CDATA[
      // finish up the hand
      oBJ.Finish();
    
      /*
      There are 4 winner and 4 loser audio files.
      Use the JS Math object to generate a random number in the range of 1..4
      and map the result to an audio file of the form [win|lose]0x.wav where x = [1..4]
      In the case of a push, the random number is not appended
      */
      var sRandResult = (Math.floor(Math.random()*4) + 1).toString();
      var aGameResults = new Array("lose0" + sRandResult, "win0" + sRandResult, "push");
     gsGameResult = aGameResults[oBJ.GetGameResult()];

      gsDealerTotal = oBJ.GetTotalOf("dealer").toString();
            
      gsNextDialog = "#PlayDone";
    ]]>
    </script>
   <goto expr="gsNextDialog"/>
    </block>
  </form>

   <!-- play the results, and ask the user to play again -->
  <form id="PlayDone">
    <field name="playagain">

      <prompt>
        <audio expr="gsSkinPath + '/' + gsGameResult + '.wav'"/>
        <audio expr="gsSkinPath + '/dealer01.wav'"/>
        <audio expr="gsSkinPath + '/' + gsDealerTotal + '.wav'"/>
        <break time="500"/>
        <audio expr="gsSkinPath + '/playagain01.wav'"/>
      </prompt>

<grammar mode="voice"
         root="gsl_top"
         version="1.0" tag-format="semantics/1.0"
         xml:lang="en-US"
         >
 <rule id="gsl_top" scope="public">
  <one-of>
   <item>
    <item>
     yes
    </item>
    <tag>out = "yes";</tag>
   </item>
   <item>
    <item>
     no
    </item>
    <tag>out = "no";</tag>
   </item>
  </one-of>
 </rule>

</grammar>
<grammar mode="dtmf"
         root="gsl_top"
         tag-format="semantics/1.0"
         version="1.0"
         >
 <rule id="gsl_top" scope="public">
  <one-of>
   <item>
    <item>
     1
    </item>
    <tag>out = "yes";</tag>
   </item>
   <item>
    <item>
     2
    </item>
    <tag>out = "no";</tag>
   </item>
  </one-of>
 </rule>

</grammar>



      <help>
        <audio expr="gsSkinPath + '/help-se.wav'"/>
        <audio expr="gsSkinPath + '/to_exit.wav'"/>
        <reprompt/>
      </help>
         
      <catch event="nomatch noinput">
        <goto expr="gsPlayState"/>
      </catch>

      <filled>
        <if cond="'yes'==playagain">
          <audio expr="gsSkinPath + '/play01.wav'"/>
          <goto next="#EvalDeal"/>
        <elseif cond="'no'==playagain"/>
          <audio expr="gsSkinPath + '/goodbye.wav'"/>
          <exit/>
        </if>
      </filled>

    </field>
  </form>
  
</vxml>

The JavaScript code for this example follows:

/* 
B J . J S - Encapsulates a game of Blackjack. Can be used anywhere JavaScript is supported -
           in a Voice application, in a Web application, etc.

a card object consists of
num - an absolute number from 1..52
type - 2..10, j, q, k, a
value - 2...11
suit - c (clubs), h (hearts), s (spades), d (diamonds)
*/
function Card(iNum, sType, iValue, sSuit)
{
  this.num = iNum;
  this.type = sType;
  this.value = iValue;
  this.suit = sSuit;
}

// Diagnostic
Card.prototype.Dump = function()
{
  return "num=" + this.num + ", type=" + this.type + ", value=" + this.value + ", suit=" + this.suit;
}

// return the card's value
Card.prototype.GetValue = function()
{
  return this.value;
}

// return the card's type
Card.prototype.GetType = function()
{
  return this.type;
}

// return the concatenation of cards type and suit (e.g. "as" == "ace of spades")
// the audio files for each card use this naming scheme
Card.prototype.GetTypeSuit = function()
{
  return this.type + this.suit;
}

// a blackjack object
function Blackjack()
{
  /*
  a tuple consisting of card type and its corresponding value
  a card is picked at random from 1 to 52
  the random value is normalized down to 0..12 to determine its corresponding tuple
  */
  this.aCardInfos = new Array(
 ["a", 11], // 0
 ["2", 2],  // 1
 ["3", 3],  // 2
 ["4", 4],
 ["5", 5],
 ["6", 6],
 ["7", 7],
 ["8", 8],
 ["9", 9],
 ["10", 10],
 ["j", 10],
 ["q", 10],
 ["k", 10]); // 12
}

// Initialize the array of dealt cards and each player's hand
Blackjack.prototype.Init = function()
{
   this.aDealtCards = new Array(); // cards dealt to the dealer and player

   this.hPlayers = {
      "caller" : [], // array of cards dealt to the player
      "dealer" : []}; // array of cards dealt to the dealer

   this.iIterations = 0; // number of iterations to generate valid card
}

// deal a card to sWho
Blackjack.prototype.Hit = function(sWho)
{
  var aHand = this.hPlayers[sWho];
  if (!aHand)
  {
     return null;
  }

  var oCard = this.GetCard();
  aHand[aHand.length] = oCard;
  this.aDealtCards[this.aDealtCards.length] = oCard;
  return oCard;
}

// return the last card dealt to sWho
Blackjack.prototype.GetLastCardOf = function(sWho)
{
  var aHand = this.hPlayers[sWho];
  if (!aHand)
  {
     return null;
  }
  return aHand[aHand.length-1];
}

// return the total value of the players hand
Blackjack.prototype.GetTotalOf = function(sWho)
{
  var aHand = this.hPlayers[sWho];
  if (!aHand)
  {
     return 0;
  }

  var iAces = 0, iTotal = 0;
  for(var i=0; i<aHand.length; i++)
  {
     if(aHand[i].value == 11) {
        iAces++;
     }
   
     iTotal += aHand[i].value;
  }

  // if the players total is above 21, count aces as low (aka 1 point instead of 11)
  // but only until the players score is below 21
  while(iTotal > 21 && iAces > 0)
  {
     iAces--;
     iTotal -= 10;
  }

  return iTotal;
}

// manufacture a random card from a deck of 52.
Blackjack.prototype.GetCard = function()
{
  var aDealtCards = this.aDealtCards;  
  var sSuit = "d";
  
  // make a card by picking a number at random from 1..52
  var iNum = Math.floor(Math.random()*52) + 1;

  // make sure the card hasn't already been dealt
  for(var i=0; i<aDealtCards.length; i++)
  {
     ++this.iIterations;
     if(iNum == aDealtCards[i].num)
     {
        i=0;
        iNum = Math.floor(Math.random()*52) + 1;
     }
  }

  // normalize the card's number from 0 (A) .. 11 (J), 12 (Q), 12 (K)
  var iCardInfoIndex = iNum % 13;

  // map the normalized number to a (type,value) tuple
  var aCard = this.aCardInfos[iCardInfoIndex];
    
  // assign a suit
  if(iNum >= 1 && iNum <= 13) {
     sSuit = 'd';  
  }
  else if(iNum >= 14 && iNum <= 26) {
     sSuit = 'c';  
  }
  else if(iNum >= 17 && iNum <= 39) {
     sSuit = 'h';
  }
  else if(iNum >= 40 && iNum <= 52) {
     sSuit = 's';
  }
  
  return new Card(iNum, aCard[0], aCard[1], sSuit);
}

// Finish up the hand
Blackjack.prototype.Finish = function()
{
  var iDealerTotal = this.GetTotalOf("dealer");
  // dealer must take cards until she hits at least 17   
  while(iDealerTotal < 17)
  {
     oBJ.Hit("dealer");
     iDealerTotal = oBJ.GetTotalOf("dealer");
  }

  return true;
}

// 0 == lose, 1 == win, 2 == draw/push
Blackjack.prototype.GetGameResult = function()
{
  var iPlayerTotal = this.GetTotalOf("caller");
  var iDealerTotal = this.GetTotalOf("dealer");

  // figure out who won
  if (iPlayerTotal > 21 && iDealerTotal > 21)
  {
     return 0;
  }   
  else if (iPlayerTotal > 21 && iDealerTotal <= 21)
  {
     return 0;
  }
  else if (iPlayerTotal <= 21 && iDealerTotal > 21)
  {
     return 1;
  }  
  else // (iPlayerTotal <= 21 && iDealerTotal <= 21)
  {
     if(iDealerTotal == iPlayerTotal)
     {
        return 2;
     }
     else if (iDealerTotal > iPlayerTotal)
     {
        return 0;
     }
     else // (iDealerTotal < iPlayerTotal)
     {
        return 1;
     }
  }
}


[24]7 Inc.| Terms of Service| Privacy Policy| General Disclaimers