Post by Acid Burn on Jun 14, 2006 12:30:59 GMT
Well as i have been bringing out a load of ASP based games i thought that i might as well share my wisdom lol.
The very powerful and extensible ASP-based multiplayer game engine we write in this article is not for the 3-D shooter, but it will suit almost all turn-based desk games. For example, to apply it to a card game or the game of chess, all you need to do is add specific logic and design. “Engine” is an abstract thing, so we will use a tic-tac-toe game since it has very simple logic and will allow us to pay more attention to the engine.
ASP is not just the glue of ActiveX objects and databases but independent server-side web applications as well. This article will show the power of VBScript programming language and more. This engine does not use any ActiveX objects, JAVA applets, DHTML, databases, and transactions – just pure ASP programming. Of course, you probably will use all of the above techniques in your application to implement logic and design.
For example, logic is not trivial in the game of chess and will require server-side ActiveX object(s) written with more powerful tools; movement of figures on the board will also require some kind of client-side scripting and/or ActiveX/Java applets. But, remember, client applet(s) support is not a feature of all browsers. Good ASP applications should work with most browsers.
The six steps involved in developing this application are:
Global variables, start page, game preparation manager, and passive players’ room. (Almost 1/3 of our application will be written here.)
Active player’s room and main logic manager.
First implementation of “Game Over” page.
Aborting the game
Protection from malicious users.
Additional features: showroom, advanced design, etc.
Step 1 : Global variables (global.asa)
We will store all global variables in an Application object and, therefore, initialize them in an Application_OnStart event in global.asa. User-specific parameters will be stored in a Session object and initialized in a Session_OnStart event.
First we will define the players. There are 2 players in the tic-tac-toe game – the first plays for “0” and the second for “X.” We define two arrays – the first for player nicknames and the second for unique IDs, and initialize them to empty/null values. We will use these unique IDs for validation purposes - SessionID will suite fine.
dim plIDs(2)
plIDs(1)=0
plIDs(2)=0
dim plNames(2)
plNames(1)=""
plNames(2)=""
Now we need a game board. In tic-tac-toe it’s a field divided by 9 cells, each of them can be either “0,” “X,” or empty. We will use integer values –1 for “0”, 1 for “X,” and 0 for an empty cell.
dim field(3, 3)
For i = 0 To 2
For j = 0 to 2
field(i, j) = 0
Next
Next
We also need visual interpretation for these values. At this point we will use text and replace text with images at step 6.
dim picts(3)
picts(0) = "0"
picts(1) = "_"
picts(2) = "X"
Note:
Be very careful when you store arrays in Application and Session objects. You can’t access the element of the array directly from these objects, like Application(“field”)(2). Instead, you should use following code for storing
Dim myarr(10)
myarr(2) = “hello”
Application(“field”) = myarr
And for reading
Dim myarr
myarr = Application(“field”)
Response.Write(myarr(2))
In reference to the above note, we will store these arrays with following code:
Application("field") = field
Application("plIDs") = plIDs
Application("plNames") = plNames
Application("pictures") = picts
Now we need variables to store the game state. We will use the following two variables:
First, Application("globalstatus") used for game preparation. It shows if there are vacant places for players to join the game. Tic-tac-toe game can be started only when 2 players have joined the game.
Second, Application("gamestatus") used to control game flow. It determines the active player.
' 0 - no game ;
' 1 - 1 player waiting;
' 2 - game is in progress
Application("globalstatus") = 0
' 0 - not ready;
' 1 - first player turn;
' 2 - second player turn;
Application("gamestatus") = 0
We will also store some user-specific information in the Session object. First, let’s store the player nickname. The default nickname will be “unknown.” Initialization takes place in the Session_OnStart event.
Session("nick") = "unknown"
The theory of turn-based multiplayer games is there is one active player and one or more passive players. These passive players wait for the active player to complete his turn. Upon completion, the game’s logic manager decides whose turn is next. Usually passive players can watch the playing field but can not make changes. In web applications we use client ActiveX/Java applets or server push/client pull technologies to determine when the state of the game was changed. This engine is designed to avoid client applets, so we will use client pull. “Client pull” means that the browser refreshes (reloads) the same page every n seconds.
So the second useful user-specific parameter is refresh time (default value will be 5 seconds):
Session("refresh") = 5
Note
There is also another important user parameter – Session timeout. This parameter is very useful for our game since it is the only way to determine when a user has left the game (whether user broke the connection or simply browsed to another site). You can control this parameter either via the Microsoft Management Console (MMC) or the Session.Timeout property (in minutes).
We will add code for Session_OnEnd event later.
Start page (default.asp)
On the start page we will ask the user for his/her nickname and refresh time. On submitting the form, our application:
Initializes user-specific parameters;
Checks for a vacant place and, if found, adds a user to the player list, updates Application("globalstatus"), and redirects to our game preparation manager:
<%
dim globstat
dim plIDs
dim plNames
if Request.Form("nick") <> "" then
Session("nick") = Server.HTMLEncode(Request.Form("nick"))
if IsNumeric(Request.Form("refresh")) then
If CInt(Request.Form("refresh")) > 0 Then
Session("refresh") = CInt(Request.Form("refresh"))
End If
else
Session("refresh") = 5
end if
if Application("globalstatus") < 2 then
Application.lock
globstat = Application("globalstatus")+1
plIDs = Application("plIDs")
plIDs(globstat)=Session.SessionID
plNames=Application("plNames")
plNames(globstat)=Session("nick")
Application("plIDs") = plIDs
Application("plNames") = plNames
Application("globalstatus") = globstat
Application("gamestatus") = 0
Application.unlock
Response.Redirect ("manager.asp")
end if
end if
%>
Note
We use the Server.HTMLEncode function to encode the player name and avoid problems with names containing HTML tags and special symbols like ‘<’ and ‘>’.
The start page also shows the current game state so the user can see if there is a vacant place to join the game.
<%
plNames=Application("plNames")
Select Case Application("globalstatus")
Case 0 Response.Write(" no players")
Case 1 Response.Write(" 1 player waiting - " & plNames(1))
Case 2 Response.Write(" game is in progress - " & plNames(1) & " vs " & plNames(2))
End Select
%>
Game preparation manager (manager.asp)
The game preparation manager is the place where players wait for each other before the game starts. In our case, the game will start only when 2 players have joined. Here we can see the first example of “client pull” technology. We use the META HTTP-EQUIV="Refresh" tag with a parameter from the Session("refresh") variable.
<META HTTP-EQUIV="Refresh" CONTENT="<% = Session("refresh") %>; URL= manager.asp">
Players will reload the same page until a minimum number of players are reached. It is monitored via Application("globalstatus"), which is changed in default.asp by new players. When this number is reached, the game manager prepares the game field and selects active player via the Application("gamestatus") variable. After this, all players are redirected to passive players’ room (wait.asp). Don’t worry – active player will be redirected to its room later.
<%
if Application("globalstatus") = 2 then
if Application("gamestatus") = 0 then
Application.lock
Application("gamestatus") = 1
dim field(3, 3)
For i = 0 To 2
For j = 0 to 2
field(i, j) = 0
Next
Next
Application("field") = field
Application.unlock
end if
Response.Redirect("wait.asp")
end if
%>
Passive players’ room (wait.asp)
Once again, passive players can watch the playing field but can not make changes. Our playing field is a 3 rows by 3 columns table, each cell of which contains either a “0” or “X” mark or is empty. We will draw this table based on the Application(“field”) array. This is the array of integers (the visual equivalents for these numbers are in array Application("pictures")). This will allow us to replace text with images easily.
<!--#include file="utils.asp"-->
<%
dim plNames
plNames=Application("plNames")
dim field
field = Application("field")
dim pic
pic = Application("pictures")
dim face
if checkwho(Session.SessionID)=1 then
face = 0 ' "0"
else
face = 2 ' "X"
end if
%>
<TABLE BORDER=1 ALIGN="Center" CELLPADDING=0 CELLSPACING=0>
<%
For i = 0 to 2
Response.Write("<TR>")
For j = 0 to 2
Response.Write("<td width='20' height='20' " &_
align='CENTER'>" & pic(field(i,j)+1) &_
"</TD>")
Next
Response.Write("</TR>")
Next
%>
In the above code we’ve used the utility function checkwho(). This is a small but very important function that helps determine from which player a request came. With this function we can see whether this player plays for “0” or for “X,” or if he is not a player at all. The implementation of this function is very simple – it just compares parameter (usually Session.SessionID) with values in the Application(“plIDs”) array. This function is located in utils.asp. We include this file to wait.asp with SSI.
<SCRIPT LANGUAGE=VBScript RUNAT=Server>
' 1 - first player; 2 - second; 0 - unknown
Function checkwho(sessionid)
dim plIDs
plIDs = Application("plIDs")
if sessionid = plIDs(1) then
checkwho = 1
elseif sessionid = plIDs(2) then
checkwho = 2
else checkwho = 0
end if
End Function
</SCRIPT>
On the wait.asp page we also show the names of players and tell the user that it’s his opponent’s turn
<TITLE>
<%
Response.Write("Wait - game is in progress - " &_
plNames(1) & " vs " & plNames(2))
%>
</TITLE>
Hey <B><%= Session("nick")%></B> it's
<font color=Red>your opponent’s</font> turn !
You play for <%= pic(face) %><br>
And finally this is a "wait" page. This means that a player wait here for another user , so we need “client pull” again.
Note
Client pull with the META HTTP-EQUIV="Refresh" tag has one disadvantage. The browser refreshes the page exactly after a specified period, even if the page was not downloaded completely. In this case (if your page has rich design or the user has a slow connection), the player will not see the whole page. To avoid this problem we can use a small sub on the client JavaScript. We will use onLoad event of BODY. This event is fired only when the whole page was received. Of course, this solution requires support of the JavaScript from the browser side. If you are not sure that the browser supports it, use the previous method.
Referring to the above note, we will add following code to the wait.asp:
<script language="JavaScript">
function GotoUrl(url)
{
parent.location=url;
}
function TimeUrl(delay, url)
{
setTimeout('GotoUrl("'+url+'")',delay*1000);
}
</script>
<BODY onload="TimeUrl(<%=Session("refresh")%>, 'wait.asp')">
For more complicated games with rich design and large playing fields, it’s recommended to turn on buffering:
Response.Buffer = TRUE
Well done. We have completed the first (and most difficult) step of our application. You can test it by trying to start a new game and looking at how the application waits for players, how the passive players’ room is displayed and reloaded, if other users can be seen at the start page, what would happen if they try to join the game after it was started. But be careful. At this step our application is not protected from incorrect use. Manual accessing of pages different from default.asp my cause it to work incorrectly.
Note
Don’t forget that this application uses global.asa, which means that if your application resides more than one level from the root, you should open properties of this folder in MMC and press the “Create Application” button.
Note
ASP distinguishes users and tracks state via cookies. So if you want to play such multiplayer games from the same computer (i.e., for testing purposes), then:
With Win NT IE, start new instance of browser instead of using "open new window"
With Win 95 IE, try to use different host names / IPs (i.e., www.mysite.com <-> ?.?.?.? ; localhost <-> 127.0.0.1 <-> mycomputername)
Step 2 : Active player’s room (yourturn.asp)
Active player is the player whose turn it is now. In our game this player should select an empty cell and place his/her sign (either “0” or “X”) there. Our active player’s room shows the same playing field as the passive players’ room, with few exceptions:
Clicking on empty cells will call our main logic manager (recalc.asp) with the player’s choice as the parameter.
The active player’s room doesn’t reload itself . There is only one active player in our engine, and nobody else can change the playing field.
<!-- #include file = "utils.asp" -->
<%
...
dim plNames
plNames=Application("plNames")
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<META HTTP-EQUIV="Pragma" CONTENT="no-proxy">
<HTML>
<HEAD>
<TITLE>
<%
Response.Write("Your turn - game is in progress - " &_
plNames(1) & " vs " & plNames(2))
%>
</TITLE></HEAD>
<BODY>
<%
dim field ' your
field = Application("field")
dim pic
pic = Application("pictures")
dim face
if checkwho(Session.SessionID)=1 then
face = 0 ' "0"
else
face = 2 ' "X"
end if
%>
Hey <B><%= Session("nick")%></B> it's
<font color=Green>your</font> turn !
You play for <b><%= pic(face) %></b><br>
<TABLE BORDER=1 ALIGN="Center" CELLPADDING=0 CELLSPACING=0>
<%
For i = 0 to 2
Response.Write("<TR>")
For j = 0 to 2
Response.Write("<td width='20' height='20' " &_ "
align='CENTER'>")
if field(i,j)=0 then
Response.Write("<A HREF= 'recalc.asp")
Response.Write("?row="&i&"&col="&j&"'>" &_
pic(field(i,j)+1)&"
Response.Write("</A></TD>")
Else
Response.Write(pic(field(i,j)+1) & "</TD>")
end if
Next
Response.Write("</TR>")
Next
%>
</TABLE>
</BODY>
</HTML>
As you can see the code is very similar to the code from wait.asp. Blank cells are links to the game logic manager with cell coordinates (row and column) as parameters.
Main logic manager (recalc.asp)
The main logic manager is the place where our application controls game flow. It modifies the playing field, updates internal state variables, chooses new active players, and, at last, determines the winner/looser. This page is called from the active player’s room, with parameters determining player’s choice. In our game these parameters are the coordinates of the cell. Using this information, the manager updates this cell or playing field with a mark identifying from which player the request came (it uses our checkwho() utility function).
<%
who = checkwho(Session.SessionID)
row = Request.QueryString("row")
col = Request.QueryString("col")
dim field
field = Application("field")
if who = 1 then
field(row, col) = -1
else
field(row, col) = 1
end if
Application.lock
Application("field")=field
After this we should check whether the game was finished or not. This job is done by the function checkwinner() that accepts the tic-tac-toe playing field array as a parameter. This function is a VBScript remake of code from the CGI Developer’s Guide by Eugene Eric Kim, (Sams.net Publishing, 1996).
<SCRIPT LANGUAGE=VBScript RUNAT=Server>
' 0 = go on
' 1 = X wins
' -1 = 0 wins
' 2 = stalemate
Function checkwinner(board)
for j = 0 to 2 ' horiz
if board(0,j)+board(1,j)+board(2,j) = 3 then
checkwinner=1
Exit Function
elseif board(0,j)+board(1,j)+board(2,j) = -3 then
checkwinner=-1
Exit Function
end if
Next
for i = 0 to 2 ' vert
if board(i,0)+board(i,1)+board(i,2) = 3 then
checkwinner=1
Exit Function
elseif board(i,0)+board(i,1)+board(i,2) = -3 then
checkwinner=-1
Exit Function
end if
Next
'diagonals
i = board(0,0)+board(1,1)+board(2,2)
j = board(2,0)+board(1,1)+board(0,2)
if (i=3) OR (j=3) then
checkwinner=1
Exit Function
elseif (i=-3) OR (j=-3) then
checkwinner=-1
Exit Function
end if
for j=0 to 2
for i=0 to 2
if board(i,j)=0 then
checkwinner=0
Exit Function
end if
Next
Next
checkwinner=2
End Function
</SCRIPT>
Now we can call this function, and depending on the result, we will do one of the following:
Change the active player (via Application("gamestatus") variable) and redirect the current player to the passive players’ room, if the function tells us to go on (returns 0).
Display the result of the game if the function reports "stalemate" or "player n has won."
dim result
result = checkwinner(field)
if result = 0 then
if Application("gamestatus") = 1 then
Application("gamestatus") = 2
else
Application("gamestatus") = 1
end if
Application.unlock
Response.Redirect("wait.asp")
end if
if result=2 then
Response.Write("Stalemate")
else
Response.Write("Congratulation! You have won the game.")
end if
%>
We will also add few lines to the passive players’ room in order to give these players a choice to join the active player’s room when the game logic manager switches active player via Application(“gamestatus”).
<!--#include file="utils.asp"-->
<%
If Application("gamestatus") = checkwho(Session.SessionID) then
Response.Redirect("yourturn.asp")
end if
All right, we have completed the main part of our game. It works! You can even play one set. But it’s still incomplete and has the following problems:
Only players who have completed their last turn will see the results.
The game is not returned to a "no players" state after the game is finished.
These and other problems will be solves in the next steps.
Step 3 : Game Over! (finish.asp)
Let’s look closer at the first problem. We show the result of the game on the recalc.asp page. But only active players can call this page, so other users will not see the result. We need a special “game over” page.
First we will make some changes to the global.asa:
Application(“gamestatus”) may accept a new value “-1,” meaning the game is over.
New variable Application(“reason”) explains why the game has finished
' 0 - preparing
' 1 - first player turn
' 2 - second player turn
' -1 game over
Application("gamestatus") = 0
' 0 - no reason
' 1 - first player won
' 2 - second player
Application("reason")=0 won; 3 - stalemate
Now we should modify recalc.asp to support the changes in variables.
Application("gamestatus") = -1 'game over
if result=-1 then
Application("reason") = result + 2 ‘ “0” won
else
Application("reason") = result + 1 ‘ “X” won or stalemate
end if
Application.unlock
Response.Redirect("finish.asp")
As you can see, game status will be updated. The application(“reason”) variable will contain the result and the active player will be redirected to our “game over” page. In order to let other players see the result, we add a few lines to wait.asp and yourturn.asp (it’s not extremely necessary to modify yourturn.asp, because active player will already be redirected ).
if Application("gamestatus") = -1 then
Response.Redirect("finish.asp")
end if
Now let’s discuss the second problem. After the game was finished we should give other users a chance to play. Our application should restore its state to “no players.” But there is one question: what does “game is finished” mean? The easiest answer is “the game is finished when the application knows the result.” But after some considerations you will choose another answer: “game is finished only when all players were informed about the result.” Only after this can we let other users start new game.
At last we can implement the first version of the “game over” page - finish.asp. It’s very similar to the passive players’ room, with few exceptions:
We tell the player the result of game.
This script contains code that removes player (who accessed this page) from the players’ ID list. Now the application knows that this player has seen the result. When all players are removed (i.e., they have seen the result), the application switches to “no players” mode, and a new game can be started.
Application.lock
plIDs(who)=0
Application("plIDs") = plIDs
if plIDs(1)=0 AND plIDs(2)=0 then Application("globalstatus")=0
Application.unlock
This page doesn’t reload itself.
A “back” link was added to return to the start page.
We have successfully solved our problems with displaying results and a resetting state. But all this stuff works only with normal game flow. But what should a player do if he/she wants to exit the game immediately or if a player has lost his/her connection for a long time. Our next step is to handle such situations.
Step 4 : Aborting the game (finish.asp and global.asa)
In a real multiplayer game we should give players a way to terminate the game at any moment, no matter if he is an active or passive player. In real Internet-based games we should be aware of loosing connection between player and server for a long time.
Note
In our engine we assume that a game is terminated when one of the players has left the game. Your rules may differ, but there are no games that can continue with just one player. And information from this section will be helpful even in this situation.
First we will add new meaning to the Application("reason") variable. Now this variable may accept new values: “-1,” meaning aborted by player 1, and “-2” for aborted by player 2.
' 0 - no reason
' 1 - first player won
' 2 - second player won
' 3 - stalemate
' -1 - aborted by player 1
' -2 - aborted by player 2
Application("reason")=0
There are three places where player may want to abort the game: the game preparation manager (manager.asp), the passive players’ room (wait.asp), and the active players’ room (yourturn.asp). We will use the same Game Over page (finish.asp) with one exception – it’s called with parameter “action = abort” to force game termination. So the following string will be added to manager.asp, wait.asp, and yourturn.asp:
<A HREF="finish.asp?action=abort">Abort waiting</A><BR>
Now in finish.asp we should separate situations when it’s called without parameter and with parameter. When it’s called with abort parameter, we should also check from where it was called:
If called from the game preparation manager, then we simply restore the "no players" state and bring the player back to the start page.
If called from the players’ rooms, then we terminate the game, grant victory to another player, and bring players to the game over page.
if Application("globalstatus")=1 then
plIDs(1)=0
Application("plIDs") = plIDs
Application("globalstatus")=0
Application("gamestatus") = -1
Application.unlock
Response.Redirect("default.asp")
end if
if Request.QueryString("action") = "abort" then
Application("gamestatus") = -1
Application("reason")=-who
end if
if Application("reason")<0 then
if (-Application("reason"))=1 then
face = 0 ' "0"
else
face = 2 ' "X"
end if
strresult = "Game aborted/timed out by player " &_
plNames(-Application("reason"))&" ("&pic(face)&")"
else
The old code for a normal end of game remains the same.
We write Internet game, which means that a player can loose his/her connection for a long time or even close the browser without bothering to terminate the game manually. These may cause other players to wait a long time for a turn, while a current player has already forgotten about the game. Thanks to ASP we can handle such situations with the session timeout feature. After a period of inactivity from a user, ASP calls Session_OnEnd event from global.asa. So we will add code here. This code is very similar to one from finish.asp (the force termination part). The only exception is that there is no need to display results or redirect the player since he/she is already gone.
The very powerful and extensible ASP-based multiplayer game engine we write in this article is not for the 3-D shooter, but it will suit almost all turn-based desk games. For example, to apply it to a card game or the game of chess, all you need to do is add specific logic and design. “Engine” is an abstract thing, so we will use a tic-tac-toe game since it has very simple logic and will allow us to pay more attention to the engine.
ASP is not just the glue of ActiveX objects and databases but independent server-side web applications as well. This article will show the power of VBScript programming language and more. This engine does not use any ActiveX objects, JAVA applets, DHTML, databases, and transactions – just pure ASP programming. Of course, you probably will use all of the above techniques in your application to implement logic and design.
For example, logic is not trivial in the game of chess and will require server-side ActiveX object(s) written with more powerful tools; movement of figures on the board will also require some kind of client-side scripting and/or ActiveX/Java applets. But, remember, client applet(s) support is not a feature of all browsers. Good ASP applications should work with most browsers.
The six steps involved in developing this application are:
Global variables, start page, game preparation manager, and passive players’ room. (Almost 1/3 of our application will be written here.)
Active player’s room and main logic manager.
First implementation of “Game Over” page.
Aborting the game
Protection from malicious users.
Additional features: showroom, advanced design, etc.
Step 1 : Global variables (global.asa)
We will store all global variables in an Application object and, therefore, initialize them in an Application_OnStart event in global.asa. User-specific parameters will be stored in a Session object and initialized in a Session_OnStart event.
First we will define the players. There are 2 players in the tic-tac-toe game – the first plays for “0” and the second for “X.” We define two arrays – the first for player nicknames and the second for unique IDs, and initialize them to empty/null values. We will use these unique IDs for validation purposes - SessionID will suite fine.
dim plIDs(2)
plIDs(1)=0
plIDs(2)=0
dim plNames(2)
plNames(1)=""
plNames(2)=""
Now we need a game board. In tic-tac-toe it’s a field divided by 9 cells, each of them can be either “0,” “X,” or empty. We will use integer values –1 for “0”, 1 for “X,” and 0 for an empty cell.
dim field(3, 3)
For i = 0 To 2
For j = 0 to 2
field(i, j) = 0
Next
Next
We also need visual interpretation for these values. At this point we will use text and replace text with images at step 6.
dim picts(3)
picts(0) = "0"
picts(1) = "_"
picts(2) = "X"
Note:
Be very careful when you store arrays in Application and Session objects. You can’t access the element of the array directly from these objects, like Application(“field”)(2). Instead, you should use following code for storing
Dim myarr(10)
myarr(2) = “hello”
Application(“field”) = myarr
And for reading
Dim myarr
myarr = Application(“field”)
Response.Write(myarr(2))
In reference to the above note, we will store these arrays with following code:
Application("field") = field
Application("plIDs") = plIDs
Application("plNames") = plNames
Application("pictures") = picts
Now we need variables to store the game state. We will use the following two variables:
First, Application("globalstatus") used for game preparation. It shows if there are vacant places for players to join the game. Tic-tac-toe game can be started only when 2 players have joined the game.
Second, Application("gamestatus") used to control game flow. It determines the active player.
' 0 - no game ;
' 1 - 1 player waiting;
' 2 - game is in progress
Application("globalstatus") = 0
' 0 - not ready;
' 1 - first player turn;
' 2 - second player turn;
Application("gamestatus") = 0
We will also store some user-specific information in the Session object. First, let’s store the player nickname. The default nickname will be “unknown.” Initialization takes place in the Session_OnStart event.
Session("nick") = "unknown"
The theory of turn-based multiplayer games is there is one active player and one or more passive players. These passive players wait for the active player to complete his turn. Upon completion, the game’s logic manager decides whose turn is next. Usually passive players can watch the playing field but can not make changes. In web applications we use client ActiveX/Java applets or server push/client pull technologies to determine when the state of the game was changed. This engine is designed to avoid client applets, so we will use client pull. “Client pull” means that the browser refreshes (reloads) the same page every n seconds.
So the second useful user-specific parameter is refresh time (default value will be 5 seconds):
Session("refresh") = 5
Note
There is also another important user parameter – Session timeout. This parameter is very useful for our game since it is the only way to determine when a user has left the game (whether user broke the connection or simply browsed to another site). You can control this parameter either via the Microsoft Management Console (MMC) or the Session.Timeout property (in minutes).
We will add code for Session_OnEnd event later.
Start page (default.asp)
On the start page we will ask the user for his/her nickname and refresh time. On submitting the form, our application:
Initializes user-specific parameters;
Checks for a vacant place and, if found, adds a user to the player list, updates Application("globalstatus"), and redirects to our game preparation manager:
<%
dim globstat
dim plIDs
dim plNames
if Request.Form("nick") <> "" then
Session("nick") = Server.HTMLEncode(Request.Form("nick"))
if IsNumeric(Request.Form("refresh")) then
If CInt(Request.Form("refresh")) > 0 Then
Session("refresh") = CInt(Request.Form("refresh"))
End If
else
Session("refresh") = 5
end if
if Application("globalstatus") < 2 then
Application.lock
globstat = Application("globalstatus")+1
plIDs = Application("plIDs")
plIDs(globstat)=Session.SessionID
plNames=Application("plNames")
plNames(globstat)=Session("nick")
Application("plIDs") = plIDs
Application("plNames") = plNames
Application("globalstatus") = globstat
Application("gamestatus") = 0
Application.unlock
Response.Redirect ("manager.asp")
end if
end if
%>
Note
We use the Server.HTMLEncode function to encode the player name and avoid problems with names containing HTML tags and special symbols like ‘<’ and ‘>’.
The start page also shows the current game state so the user can see if there is a vacant place to join the game.
<%
plNames=Application("plNames")
Select Case Application("globalstatus")
Case 0 Response.Write(" no players")
Case 1 Response.Write(" 1 player waiting - " & plNames(1))
Case 2 Response.Write(" game is in progress - " & plNames(1) & " vs " & plNames(2))
End Select
%>
Game preparation manager (manager.asp)
The game preparation manager is the place where players wait for each other before the game starts. In our case, the game will start only when 2 players have joined. Here we can see the first example of “client pull” technology. We use the META HTTP-EQUIV="Refresh" tag with a parameter from the Session("refresh") variable.
<META HTTP-EQUIV="Refresh" CONTENT="<% = Session("refresh") %>; URL= manager.asp">
Players will reload the same page until a minimum number of players are reached. It is monitored via Application("globalstatus"), which is changed in default.asp by new players. When this number is reached, the game manager prepares the game field and selects active player via the Application("gamestatus") variable. After this, all players are redirected to passive players’ room (wait.asp). Don’t worry – active player will be redirected to its room later.
<%
if Application("globalstatus") = 2 then
if Application("gamestatus") = 0 then
Application.lock
Application("gamestatus") = 1
dim field(3, 3)
For i = 0 To 2
For j = 0 to 2
field(i, j) = 0
Next
Next
Application("field") = field
Application.unlock
end if
Response.Redirect("wait.asp")
end if
%>
Passive players’ room (wait.asp)
Once again, passive players can watch the playing field but can not make changes. Our playing field is a 3 rows by 3 columns table, each cell of which contains either a “0” or “X” mark or is empty. We will draw this table based on the Application(“field”) array. This is the array of integers (the visual equivalents for these numbers are in array Application("pictures")). This will allow us to replace text with images easily.
<!--#include file="utils.asp"-->
<%
dim plNames
plNames=Application("plNames")
dim field
field = Application("field")
dim pic
pic = Application("pictures")
dim face
if checkwho(Session.SessionID)=1 then
face = 0 ' "0"
else
face = 2 ' "X"
end if
%>
<TABLE BORDER=1 ALIGN="Center" CELLPADDING=0 CELLSPACING=0>
<%
For i = 0 to 2
Response.Write("<TR>")
For j = 0 to 2
Response.Write("<td width='20' height='20' " &_
align='CENTER'>" & pic(field(i,j)+1) &_
"</TD>")
Next
Response.Write("</TR>")
Next
%>
In the above code we’ve used the utility function checkwho(). This is a small but very important function that helps determine from which player a request came. With this function we can see whether this player plays for “0” or for “X,” or if he is not a player at all. The implementation of this function is very simple – it just compares parameter (usually Session.SessionID) with values in the Application(“plIDs”) array. This function is located in utils.asp. We include this file to wait.asp with SSI.
<SCRIPT LANGUAGE=VBScript RUNAT=Server>
' 1 - first player; 2 - second; 0 - unknown
Function checkwho(sessionid)
dim plIDs
plIDs = Application("plIDs")
if sessionid = plIDs(1) then
checkwho = 1
elseif sessionid = plIDs(2) then
checkwho = 2
else checkwho = 0
end if
End Function
</SCRIPT>
On the wait.asp page we also show the names of players and tell the user that it’s his opponent’s turn
<TITLE>
<%
Response.Write("Wait - game is in progress - " &_
plNames(1) & " vs " & plNames(2))
%>
</TITLE>
Hey <B><%= Session("nick")%></B> it's
<font color=Red>your opponent’s</font> turn !
You play for <%= pic(face) %><br>
And finally this is a "wait" page. This means that a player wait here for another user , so we need “client pull” again.
Note
Client pull with the META HTTP-EQUIV="Refresh" tag has one disadvantage. The browser refreshes the page exactly after a specified period, even if the page was not downloaded completely. In this case (if your page has rich design or the user has a slow connection), the player will not see the whole page. To avoid this problem we can use a small sub on the client JavaScript. We will use onLoad event of BODY. This event is fired only when the whole page was received. Of course, this solution requires support of the JavaScript from the browser side. If you are not sure that the browser supports it, use the previous method.
Referring to the above note, we will add following code to the wait.asp:
<script language="JavaScript">
function GotoUrl(url)
{
parent.location=url;
}
function TimeUrl(delay, url)
{
setTimeout('GotoUrl("'+url+'")',delay*1000);
}
</script>
<BODY onload="TimeUrl(<%=Session("refresh")%>, 'wait.asp')">
For more complicated games with rich design and large playing fields, it’s recommended to turn on buffering:
Response.Buffer = TRUE
Well done. We have completed the first (and most difficult) step of our application. You can test it by trying to start a new game and looking at how the application waits for players, how the passive players’ room is displayed and reloaded, if other users can be seen at the start page, what would happen if they try to join the game after it was started. But be careful. At this step our application is not protected from incorrect use. Manual accessing of pages different from default.asp my cause it to work incorrectly.
Note
Don’t forget that this application uses global.asa, which means that if your application resides more than one level from the root, you should open properties of this folder in MMC and press the “Create Application” button.
Note
ASP distinguishes users and tracks state via cookies. So if you want to play such multiplayer games from the same computer (i.e., for testing purposes), then:
With Win NT IE, start new instance of browser instead of using "open new window"
With Win 95 IE, try to use different host names / IPs (i.e., www.mysite.com <-> ?.?.?.? ; localhost <-> 127.0.0.1 <-> mycomputername)
Step 2 : Active player’s room (yourturn.asp)
Active player is the player whose turn it is now. In our game this player should select an empty cell and place his/her sign (either “0” or “X”) there. Our active player’s room shows the same playing field as the passive players’ room, with few exceptions:
Clicking on empty cells will call our main logic manager (recalc.asp) with the player’s choice as the parameter.
The active player’s room doesn’t reload itself . There is only one active player in our engine, and nobody else can change the playing field.
<!-- #include file = "utils.asp" -->
<%
...
dim plNames
plNames=Application("plNames")
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<META HTTP-EQUIV="Pragma" CONTENT="no-proxy">
<HTML>
<HEAD>
<TITLE>
<%
Response.Write("Your turn - game is in progress - " &_
plNames(1) & " vs " & plNames(2))
%>
</TITLE></HEAD>
<BODY>
<%
dim field ' your
field = Application("field")
dim pic
pic = Application("pictures")
dim face
if checkwho(Session.SessionID)=1 then
face = 0 ' "0"
else
face = 2 ' "X"
end if
%>
Hey <B><%= Session("nick")%></B> it's
<font color=Green>your</font> turn !
You play for <b><%= pic(face) %></b><br>
<TABLE BORDER=1 ALIGN="Center" CELLPADDING=0 CELLSPACING=0>
<%
For i = 0 to 2
Response.Write("<TR>")
For j = 0 to 2
Response.Write("<td width='20' height='20' " &_ "
align='CENTER'>")
if field(i,j)=0 then
Response.Write("<A HREF= 'recalc.asp")
Response.Write("?row="&i&"&col="&j&"'>" &_
pic(field(i,j)+1)&"
Response.Write("</A></TD>")
Else
Response.Write(pic(field(i,j)+1) & "</TD>")
end if
Next
Response.Write("</TR>")
Next
%>
</TABLE>
</BODY>
</HTML>
As you can see the code is very similar to the code from wait.asp. Blank cells are links to the game logic manager with cell coordinates (row and column) as parameters.
Main logic manager (recalc.asp)
The main logic manager is the place where our application controls game flow. It modifies the playing field, updates internal state variables, chooses new active players, and, at last, determines the winner/looser. This page is called from the active player’s room, with parameters determining player’s choice. In our game these parameters are the coordinates of the cell. Using this information, the manager updates this cell or playing field with a mark identifying from which player the request came (it uses our checkwho() utility function).
<%
who = checkwho(Session.SessionID)
row = Request.QueryString("row")
col = Request.QueryString("col")
dim field
field = Application("field")
if who = 1 then
field(row, col) = -1
else
field(row, col) = 1
end if
Application.lock
Application("field")=field
After this we should check whether the game was finished or not. This job is done by the function checkwinner() that accepts the tic-tac-toe playing field array as a parameter. This function is a VBScript remake of code from the CGI Developer’s Guide by Eugene Eric Kim, (Sams.net Publishing, 1996).
<SCRIPT LANGUAGE=VBScript RUNAT=Server>
' 0 = go on
' 1 = X wins
' -1 = 0 wins
' 2 = stalemate
Function checkwinner(board)
for j = 0 to 2 ' horiz
if board(0,j)+board(1,j)+board(2,j) = 3 then
checkwinner=1
Exit Function
elseif board(0,j)+board(1,j)+board(2,j) = -3 then
checkwinner=-1
Exit Function
end if
Next
for i = 0 to 2 ' vert
if board(i,0)+board(i,1)+board(i,2) = 3 then
checkwinner=1
Exit Function
elseif board(i,0)+board(i,1)+board(i,2) = -3 then
checkwinner=-1
Exit Function
end if
Next
'diagonals
i = board(0,0)+board(1,1)+board(2,2)
j = board(2,0)+board(1,1)+board(0,2)
if (i=3) OR (j=3) then
checkwinner=1
Exit Function
elseif (i=-3) OR (j=-3) then
checkwinner=-1
Exit Function
end if
for j=0 to 2
for i=0 to 2
if board(i,j)=0 then
checkwinner=0
Exit Function
end if
Next
Next
checkwinner=2
End Function
</SCRIPT>
Now we can call this function, and depending on the result, we will do one of the following:
Change the active player (via Application("gamestatus") variable) and redirect the current player to the passive players’ room, if the function tells us to go on (returns 0).
Display the result of the game if the function reports "stalemate" or "player n has won."
dim result
result = checkwinner(field)
if result = 0 then
if Application("gamestatus") = 1 then
Application("gamestatus") = 2
else
Application("gamestatus") = 1
end if
Application.unlock
Response.Redirect("wait.asp")
end if
if result=2 then
Response.Write("Stalemate")
else
Response.Write("Congratulation! You have won the game.")
end if
%>
We will also add few lines to the passive players’ room in order to give these players a choice to join the active player’s room when the game logic manager switches active player via Application(“gamestatus”).
<!--#include file="utils.asp"-->
<%
If Application("gamestatus") = checkwho(Session.SessionID) then
Response.Redirect("yourturn.asp")
end if
All right, we have completed the main part of our game. It works! You can even play one set. But it’s still incomplete and has the following problems:
Only players who have completed their last turn will see the results.
The game is not returned to a "no players" state after the game is finished.
These and other problems will be solves in the next steps.
Step 3 : Game Over! (finish.asp)
Let’s look closer at the first problem. We show the result of the game on the recalc.asp page. But only active players can call this page, so other users will not see the result. We need a special “game over” page.
First we will make some changes to the global.asa:
Application(“gamestatus”) may accept a new value “-1,” meaning the game is over.
New variable Application(“reason”) explains why the game has finished
' 0 - preparing
' 1 - first player turn
' 2 - second player turn
' -1 game over
Application("gamestatus") = 0
' 0 - no reason
' 1 - first player won
' 2 - second player
Application("reason")=0 won; 3 - stalemate
Now we should modify recalc.asp to support the changes in variables.
Application("gamestatus") = -1 'game over
if result=-1 then
Application("reason") = result + 2 ‘ “0” won
else
Application("reason") = result + 1 ‘ “X” won or stalemate
end if
Application.unlock
Response.Redirect("finish.asp")
As you can see, game status will be updated. The application(“reason”) variable will contain the result and the active player will be redirected to our “game over” page. In order to let other players see the result, we add a few lines to wait.asp and yourturn.asp (it’s not extremely necessary to modify yourturn.asp, because active player will already be redirected ).
if Application("gamestatus") = -1 then
Response.Redirect("finish.asp")
end if
Now let’s discuss the second problem. After the game was finished we should give other users a chance to play. Our application should restore its state to “no players.” But there is one question: what does “game is finished” mean? The easiest answer is “the game is finished when the application knows the result.” But after some considerations you will choose another answer: “game is finished only when all players were informed about the result.” Only after this can we let other users start new game.
At last we can implement the first version of the “game over” page - finish.asp. It’s very similar to the passive players’ room, with few exceptions:
We tell the player the result of game.
This script contains code that removes player (who accessed this page) from the players’ ID list. Now the application knows that this player has seen the result. When all players are removed (i.e., they have seen the result), the application switches to “no players” mode, and a new game can be started.
Application.lock
plIDs(who)=0
Application("plIDs") = plIDs
if plIDs(1)=0 AND plIDs(2)=0 then Application("globalstatus")=0
Application.unlock
This page doesn’t reload itself.
A “back” link was added to return to the start page.
We have successfully solved our problems with displaying results and a resetting state. But all this stuff works only with normal game flow. But what should a player do if he/she wants to exit the game immediately or if a player has lost his/her connection for a long time. Our next step is to handle such situations.
Step 4 : Aborting the game (finish.asp and global.asa)
In a real multiplayer game we should give players a way to terminate the game at any moment, no matter if he is an active or passive player. In real Internet-based games we should be aware of loosing connection between player and server for a long time.
Note
In our engine we assume that a game is terminated when one of the players has left the game. Your rules may differ, but there are no games that can continue with just one player. And information from this section will be helpful even in this situation.
First we will add new meaning to the Application("reason") variable. Now this variable may accept new values: “-1,” meaning aborted by player 1, and “-2” for aborted by player 2.
' 0 - no reason
' 1 - first player won
' 2 - second player won
' 3 - stalemate
' -1 - aborted by player 1
' -2 - aborted by player 2
Application("reason")=0
There are three places where player may want to abort the game: the game preparation manager (manager.asp), the passive players’ room (wait.asp), and the active players’ room (yourturn.asp). We will use the same Game Over page (finish.asp) with one exception – it’s called with parameter “action = abort” to force game termination. So the following string will be added to manager.asp, wait.asp, and yourturn.asp:
<A HREF="finish.asp?action=abort">Abort waiting</A><BR>
Now in finish.asp we should separate situations when it’s called without parameter and with parameter. When it’s called with abort parameter, we should also check from where it was called:
If called from the game preparation manager, then we simply restore the "no players" state and bring the player back to the start page.
If called from the players’ rooms, then we terminate the game, grant victory to another player, and bring players to the game over page.
if Application("globalstatus")=1 then
plIDs(1)=0
Application("plIDs") = plIDs
Application("globalstatus")=0
Application("gamestatus") = -1
Application.unlock
Response.Redirect("default.asp")
end if
if Request.QueryString("action") = "abort" then
Application("gamestatus") = -1
Application("reason")=-who
end if
if Application("reason")<0 then
if (-Application("reason"))=1 then
face = 0 ' "0"
else
face = 2 ' "X"
end if
strresult = "Game aborted/timed out by player " &_
plNames(-Application("reason"))&" ("&pic(face)&")"
else
The old code for a normal end of game remains the same.
We write Internet game, which means that a player can loose his/her connection for a long time or even close the browser without bothering to terminate the game manually. These may cause other players to wait a long time for a turn, while a current player has already forgotten about the game. Thanks to ASP we can handle such situations with the session timeout feature. After a period of inactivity from a user, ASP calls Session_OnEnd event from global.asa. So we will add code here. This code is very similar to one from finish.asp (the force termination part). The only exception is that there is no need to display results or redirect the player since he/she is already gone.