
C# ПІДРУЧНИКИ / c# / Hungry Minds - ASP.NET Bible VB.NET & C#
.pdfA hand in the participant is worth . . .
If the Participant class were something I felt I could generalize to use in other games as well, I could create a DLL for it and import it as I did with the PlayingCards namespace. But for now, I've just included it within the page itself, as shown here:
Class Participant
Public Hand As ArrayList
Sub New()
Hand = New ArrayList
End Sub
The most important part of this page is the Public Hand ArrayList. It is declared and then, in the constructor for the class, instantiated.
Once the Participant-type object is created, Hand can be freely accessed and manipulated using all the standard ArrayList methods. No attempt was made here (as there was with Shoe) to hide this structure and control access to it. This approach makes it easy to implement and intuitive to use, but also limits the control you'll have over this structure. In fact, you'll have no control over this structure. But, for this use, I decided that was okay — especially because I am creating it on this page for the exclusive use in this game. If you do generalize this class in the future for other games, you definitely want to revisit the question of making Hand public.
The two methods in this class provide functionality to support the Hand ArrayList, which is assumed to contain Card-type objects.
Adding up the TotalValue
The first of these methods, TotalValue, simply loops through all the cards in the Hand, sums up their value, and returns the result:
Function TotalValue As Integer
Dim i As Integer
Dim AceFlag As Boolean = False
Dim CurCard As Card
TotalValue = 0
For Each CurCard In Hand
TotalValue = TotalValue + CurCard.Value
If CurCard.Name = "Ace" Then
AceFlag = True
End If
Next
If AceFlag = True And TotalValue + 10 < 21 Then
TotalValue += 10
End If
End Function
Notice that this is the code that handles the ace's possible alternate values. The rule is that an ace can count as either 1 or 11 at the participant's option. But in reality, if you
have two aces and count them both as 11, your total is a busting 22. So you'll only ever count, at most, one ace as 11. And, even then, you'll only do it if it doesn't cause you to bust.
With that in mind, this code adds up the total of all the cards in the hand, setting a flag to true if at least one of them is an ace. The aces are, by default, counted as 1, so the code after the loop checks to see if adding an additional 10 to the total (turning one of the aces into 11 instead of 1) causes the participant to bust. If not, 10 is added to the total.
Some user interfacing: UpdateLabels
The UpdateLabels method takes the internal data structures and shows them to the user using the labels on the page.
Typically, when you create business objects, you make a strict line of division between the user interface and the business rules. In the case of the UpdateLabels method, I cheated. I'll explain why after I describe how it works.
Sub UpdateLabels(CardLabels As ArrayList, TotalLabel As Label)
Dim i As Integer
Dim CurCard As Card
Dim CurLabel As Label
For i=0 To CardLabels.Count-1
CurLabel = CType(CardLabels(i), Label)
If i <= Hand.Count-1 Then
CurCard = CType(Hand(i),Card)
CurLabel.Text = CurCard.Name & " of " & CurCard.Suit
Else
CurLabel.Text = ""
End If
Next
TotalLabel.Text = CStr(TotalValue)
End Sub
End Class
UpdateLabels accepts an ArrayList of label controls and a single label control. It then goes through all the labels in the ArrayList and fills them in with the name of the cards in this participant's Hand. Any labels that don't have corresponding cards in the Hand are set to the empty string to clear out any previous values that might be there.
Then, to top it off, UpdateLabels calls the TotalValue method and plots that information into the label sent in the second argument to this method.
This method is clearly designed to update the user interface with the current values stored in the business object. And, typically, that would disqualify it from being a part of the business object. But, in my mind, three mitigating factors justified making it a method:
§If I didn't include the subroutine in the class, I'd have two other options: I'd have to create two nearly identical subroutines on the page to update the UI for the dealer and another to update the UI for the player. That's no good. The other option is to generalize the function on the page to accept a Participant object in addition to the two other arguments and then use it to fill in the labels. That would work, but I felt this method was simpler and more straightforward.
§This class is a part of this page, rather than included in a DLL that may be used on other pages (with different user interface needs). This means that its scope and use is limited to begin with. In this case, the Participant class was created more for code organization and readability than for reuse. Therefore, the issue of blurring the line between business object and user interface is less significant.
§The method doesn't directly access the user interface. I wrote it so that it would work with any ArrayList of labels and any individual label. This means that although the method technically does a user interface task, it is not directly tied to a specific user interface implementation.
Whether you agree with the decision or not, that's the way I did it. If it bothers you, it would be relatively easy to pull the subroutine out of the class, add the Participant-type object as another argument, and tweak the code to use the Hand in the argument.
The body
I'm going to go a little out of order here. I think that when you are trying to figure out someone else's code, you should look at three things, in this order: the classes they create and use, the body of the page, and the code written to respond to page events.
The key classes, Shoe and Participant, are discussed in previous sections of this chapter. Before I dive into the event code, it's important that you take a look at the body of the page so that you can see the user interface and the controls that are likely to trigger events:
<body>
<h1>Blackjack</h1>
<p>Your goal is to try to get closer to 21 than the
dealer without going over. As you begin you have two cards
and you get to see the dealer's first card. Face cards count
as 10 and Aces can count as either 1 or 11 (whatever works
best for you). You can click Hit to draw another card or
click Stand to keep your current total.</p>
<form action="blackjack.aspx" method="post" runat="server">
<table width=100%>
<tr><td align="left" valign="top">
<h2>Your Hand</h2>
Card 1: <asp:label id="PCard1" runat="server"/><br>
Card 2: <asp:label id="PCard2" runat="server"/><br>
Card 3: <asp:label id="PCard3" runat="server"/><br>
Card 4: <asp:label id="PCard4" runat="server"/><br>
Card 5: <asp:label id="PCard5" runat="server"/><br>
Card 6: <asp:label id="PCard6" runat="server"/><br>
<br>
Your Total: <asp:label id="PTotal" runat="server"/><br><br>
<asp:button id="Hit" type=submit text=" Hit "
OnClick="Hit_Click" runat="server"/>
<asp:button id="Stand" type=submit text="Stand" OnClick=
"Stand_Click" runat="server"/>
</td>
<td align="left" valign="top">
<h2>Dealer Hand</h2>
Card 1: <asp:label id="DCard1" runat="server"/><br>
Card 2: <asp:label id="DCard2" runat="server"/><br>
Card 3: <asp:label id="DCard3" runat="server"/><br>
Card 4: <asp:label id="DCard4" runat="server"/><br>
Card 5: <asp:label id="DCard5" runat="server"/><br>
Card 6: <asp:label id="DCard6" runat="server"/><br>
<br>
Dealer Total: <asp:label id="DTotal" runat="server"/><br>
</td>
</tr>
</table>
<br>
<asp:label id="Message" runat="server"/>
<br><br>
<asp:label id="PlayAgain" runat="server" text="Play Again?" visible=false />
<asp:button id="Yes" type=submit text="Yes" OnClick="Yes_Click"
runat="server" visible=false />
<asp:button id="No" type=submit text=" No " OnClick="No_Click"
runat="server" visible=false />
</form>
</center>
</body>
After displaying the header and some instructions, a table is used to organize all the pieces on this page. The table essentially divides the page into two sections vertically. On the left is the player's side, which contains labels to show the player cards and a total. In addition, buttons are included so that the player can indicate their preference to Hit or Stand. On the right is the dealer's side, and its controls are identical, with the omission of the Hit/Stand buttons.
Below the table is a label called Message, which is used to tell the user the final result of the game — whether they won or lost.
Finally, there are three controls that are initially hidden and only made visible (in code) when the game is complete. A label asks the user if they'd like to play again, and the Yes and No buttons allow the user to respond.
Initialization and object juggling
The first three subroutines on this page are designed to initialize, store, and retrieve important information that is used from one server round trip to the next. The first two subroutines are events that are triggered in the life cycle of processing the page:
In this case, if this is the first time the page has been retrieved, the StartGame subroutine is called. Here, all the objects needed for the game are instantiated and all the variables initialized:
Sub StartGame
GameShoe = Nothing
Player = Nothing
Dealer = Nothing
GameShoe = New Shoe(1)
GameShoe.Shuffle
Player = New Participant
Dealer = New Participant
Player.Hand.Add(GameShoe.Draw)
Player.Hand.Add(GameShoe.Draw)
Dealer.Hand.Add(GameShoe.Draw)
Player.UpdateLabels(PlayerLabels, PTotal)
Dealer.UpdateLabels(DealerLabels, DTotal)
Message.Text = ""
End Sub
This subroutine is called from the Page_Load event and then again, later, if the user wants to start over and play a new game. So, in case the GameShoe, Player, and Dealer variables already contain objects, they are set to Nothing to clear them out. Then the GameShoe is instantiated as a new Shoe class. A 1 is passed to the constructor, indicating that a one-deck shoe should be created. Then the cards in the shoe are shuffled.
Player and Dealer are each instantiated as Participants.
Next the GameShoe's Draw method is called three times and the result (a Card object) is added to the hands of the player and dealer.
UpdateLabels is called separately for each participant passing the appropriate ArrayList and the label control holding the card totals. This causes the labels on the page to be updated, showing the two cards in the player's hand and the single card in the dealer's hand.
Storing and retrieving the objects from Session variables
Here's a timeline of the events that will happen on this page:
1. The page is requested and loaded for the first time. The Page_Load event occurs and the StartGame subroutine is called. This subroutine creates and instantiates the GameShoe, Player, and Dealer objects, preparing them to be ready for play.
2. The Page_PreRender event occurs. This event happens after all the server-side code has executed and just before the final HTML is sent to the browser. It's the last opportunity to do anything. I use this event to store my key objects in Session variables.
3. The first server round trip occurs. The Page_Load event is executed. The objects are retrieved from their Session variables immediately so that they'll be available to any other events that occur.
4. After all events are done, the Page_PreRender event occurs again. And again, the objects are stored away in Session variables for safekeeping until the next server round trip.
The StartGame subroutine was discussed in the previous section. The code in the Page_PreRender event looks like this:
Sub Page_PreRender(sender As Object, e As EventArgs)
Session("GameShoe") = GameShoe
Session("Player") = Player
Session("Dealer") = Dealer
End Sub
The objects are placed in Session variables and stored in the server's memory until the page needs them again.
Then, in the Page_Load event, if this is a post-back, this code executes:
Else
GameShoe = CType(Session("GameShoe"),Shoe)
Player = CType(Session("Player"),Participant)
Dealer = CType(Session("Dealer"),Participant)
End If
This code does just the opposite of the code in the Page_PreRender event — it gets the objects out of the Session variables. Because Session variables are not typed, a CType command is used to assure the proper conversion back into their associated classes.
This approach, using Page_Load and Page_PreRender to store and retrieve commonly used global variables and objects, is very handy and can be applied to any page that needs to remember things from one round-trip to the next. If you only need to store variable values, then you can use ViewState instead of Session and avoid taking up precious server memory. Unfortunately, you can't store objects in ViewState unless they implement the ISerializable interface.
Responding to events
All of the work thus far has been in an effort to get to this point: a blackjack table presented to the user and ready for them to play the game.
Now the ball is in the user's court. They look at the cards they were dealt, look at the dealer's first card, and make a decision: Hit or Stand.
Hit me
If the user has a low total, or is feeling lucky, the user is likely to click the Hit button. When the user does this, the following subroutine is executed:
Sub Hit_Click(Sender As Object, E As EventArgs)
Player.Hand.Add(GameShoe.Draw)
Player.UpdateLabels(PlayerLabels, PTotal)
If Player.TotalValue = 21 Then
Message.Text = "You Have 21...You Win!"
EndGame
ElseIf Player.TotalValue > 21
Message.Text = "You Busted...Dealer Wins!"
EndGame
End If
End Sub
A card is drawn from the GameShoe and added to the player's hand. The labels on the page are updated. That's all there is to a hit!
However, after the user has a new card, there are some conditions to check. For example, does the player have 21? If so, the player wins and the game is over. Conversely, if this draw put the user over the edge and the user's total is greater than 21, then the player has busted and therefore loses.
I believe I'll stand
After the user achieves a good total, they'll click the Stand button. That event triggers this subroutine:
Sub Stand_Click(Sender As Object, E As EventArgs)
Message. Text = "You decided to stand on " & _
CStr(Player.TotalValue)
DealerPlay
End Sub
The Message label is updated to indicate the user's decision to stand, and the value they chose to stand on. Because clicking Stand represents the end of the player's turn, the DealerPlay subroutine is called from here.
The dealer's turn
If the player gets exactly 21 or busts, the result is a forgone conclusion and there's no reason for the dealer to even play. The dealer only goes when the player decides to stand on a particular total. This is the subroutine that gets called when that happens:
Sub DealerPlay
Dealer.Hand.Add(GameShoe.Draw)
Do While Dealer.TotalValue < 17
Dealer.Hand.Add(GameShoe.Draw)
Loop
Dealer.UpdateLabels(DealerLabels, DTotal)
DetermineWinner
EndGame
End Sub
A card is drawn from the GameShoe and added to the dealer's hand. Next, a Do While loop checks to see if the total is less than 17. If it is, the dealer keeps on drawing. This is the standard strategy that dealers are required to follow in casinos.
Once this loop completes, the labels on the page are updated to show the dealer's new cards.
The DetermineWinner subroutine is called to do just that. After the DetermineWinner subroutine executes, the EndGame subroutine is called.
Who won?
The player chose to stand and the dealer did its drawing. So what's the result? The DetermineWinner subroutine does the comparisons to see who won and displays the result in the Message label:
Sub DetermineWinner
If Dealer.TotalValue = Player.TotalValue Then
Message.Text = "A Push! No Winner..."
ElseIf Dealer.TotalValue = 21 Then
Message.Text = "Dealer Has 21...Dealer Wins!"
ElseIf Dealer.TotalValue > 21 Then
Message.Text = "Dealer Busted...You Win!"
ElseIf Dealer.TotalValue > Player.TotalValue Then
Message.Text = "Dealer Wins..."
Else
Message.Text = "You Win! Congratulations!"
End If
End Sub
This code doesn't need to check for a player 21 or a player bust because those conditions were already handled in the Hit_Click subroutine. So here the only options are push, dealer with 21, player with 21, dealer wins, and player wins.
Ending the game
No matter who wins or loses, the game always ends in the same way:
Sub EndGame
Hit.Enabled = False
Stand.Enabled = False
PlayAgain.visible=True
Yes.visible= True
No.Visible=True
End Sub
The Hit and Stand buttons can't be used now, so they are disabled. However, the PlayAgain label along with the Yes and No buttons are made visible.
The label asks the user if they'd like to play again. If the answer is no, No_Click is called:
Sub No_Click(Sender As Object, E As EventArgs)
Yes.visible = False
No.visible = False
PlayAgain.text = "Thanks For Playing!"
End Sub
The buttons are made invisible again and the PlayAgain text is changed to thank the user for playing.
If the answer to PlayAgain is Yes, Yes_Click is executed:
Sub Yes_Click(Sender As Object, E As EventArgs)
Yes.visible = False
No.visible = False
PlayAgain.visible = False
Hit.Enabled = True
Stand.Enabled = True
StartGame
End Sub
All the originally invisible controls become invisible again. Hit and Stand are enabled and the StartGame subroutine is called to kick off the process once again.
Room to Grow
This chapter is a running start, but you certainly have lots of opportunities to take this project and make it your own. Here are some ideas for enhancing it:
§As already mentioned, the user interface is, well, simple. You could easily add some pizzazz by using either graphics or HTML tables to emulate the look of cards on a blackjack table.
§As for the game itself, it would be significantly more fun if you could track how you're doing on an ongoing basis. You could do that by showing wins and losses, but probably the better way to go to simulate a real casino is to give
the user a certain amount of play money when they start and allow them to choose how much of that money they want to gamble on each hand. If they wager $10 and win, they get their original amount back plus an additional $10. You could store the user's current cash in a Session variable.
§After you implement wagering, you could add some rules to give the user more options in the game. Doubling down is an option the player has only once per hand — after being dealt the first two cards. When the player doubles down, they double their bet with the understanding that they'll receive one additional card and then stand on whatever that total is. For example, if the player is dealt a 6 and a 5, for a total of 11 with their first two cards, it is a good idea to double down and hope that the next card is a face card. This allows the player to win twice as much as they would otherwise. Of course, if the next card is a 3, the player cannot hit and is forced to stand on a very weak 14.
§Splitting is another popular option that is also available only after the first two cards are dealt. In addition, you can only split if the first two cards are the same, say two 8s. When you split, you put a sum of money equal to your original bet on the table beside your original bet. The two cards are split up and form the foundation for two separate hands. The player then chooses to hit (as many times as they like) or stand, as usual, on each hand individually, trying to get close to 21 with each hand. Remember that when you split, both hands are playing against the dealer. It's possible to win both hands, lose both, or win one and lose the other.
Summary
Blackjack is a game with just enough complexity to make a coding project interesting. For this project, I used it to demonstrate many different techniques, such as how to create a dynamic user interface with Web forms and user controls and how to use an ArrayList to create a more flexible version of the stack control. Next, I covered how to create a reusable data structure and a reusable component that encapsulates all the complexity for a particular task (in this case, initializing and handling decks of playing cards). This cleans up and simplifies the code in the main page to focus on its task — the game. Then I compiled simple Visual Basic components with the command line compiler and used them within an ASP.NET page. In addition, I used classes within a page to simplify and organize the code (as I did here with Participant) and the Mod operator to process certain iterations within a loop.