Walkthrough: Creating an Internet Game with F#, C#, and Windows Azure
By reviewing this topic and the sample that accompanies it, you’ll learn how to create a web application (in this case, a word game) by using F#, C#, ASP.NET Model-View-Controller (MVC), Windows Azure, and JavaScript. This topic demonstrate numerous useful coding techniques by explaining code excerpts from the WordGrid sample. The WordGrid sample is a Windows Azure cloud service that you can build and either run locally or deploy to Windows Azure. A Windows Azure cloud service is just a web application that runs in Windows Azure. Because WordGrid is a cloud service, players can access it from anywhere in the world by using their browsers.
By following this walkthrough and studying the code that it demonstrates, you’ll learn:
How to integrate F# and C# (and JavaScript for the client) into a web application that runs in the cloud.
How to use Windows Azure storage services queues to communicate between roles in a Windows Azure cloud service.
How to use ASP.NET MVC 4 and the Razor view engine to build a web page that reflects dynamic data.
How to use F# type providers and query expressions to access and modify data.
Prerequisites
If you want to follow this walkthrough, install the WordGrid sample application. See Walkthrough: Publishing an F#/C# MVC Application to Windows Azure to get started.
This walkthrough assumes that you’ve installed the sample, connected it to a database, and run through it at least once. This walkthrough also assumes that you have some familiarity with C# and F# and that you’ve created at least one or two Windows Azure projects. If you haven’t done any work with Windows Azure, you can sign up for a free trial at Windows Azure. To learn the basic concepts of cloud service development, see walkthroughs such as Getting Started with the Windows Azure Tools for Visual Studio and Developing and Deploying Windows Azure Cloud Services Using Visual Studio.
Table of Contents
This topic contains the following sections.
Overview of the Core F# Game Code
Overview of the C# ASP.NET Code
Overview of the F# Worker Code
Overview of the F# Game Code
As the WordGrid sample shows, you can integrate F# with an ASP.NET MVC web application in a couple of ways. You can separate code into model classes, views, and controller classes by using the MVC app model. The model classes contain ordinary C# code and abstract objects that are independent of the user interface. You specify the layout of the pages in view classes. In controller classes, you specify how HTTP requests are processed and handled, which involves matching up the appropriate model data with the appropriate view, given the details of each HTTP request.
In this sample, you create the UI for the web pages by using the Razor rendering engine, which is part of ASP.NET MVC 4. Following the Razor engine syntax, you write C# code in View pages.
In this application, F# comes into the picture in two ways. First, you use F# as a library that you reference from the C# code. Then you use F# as a separate worker role that processes requests to look up words in a dictionary.
In the next section, you’ll review representative code for each section in turn, and then you’ll modify the code to improve the coding style. First, look at the F# library, which is the WordGridGame project in the solution.
This F# code abstracts the word game in a typical way for object-oriented programming. The code consists of several class types that represent the basic elements of the word game, the tile, player, the game board, and the game itself.
As an example, consider the Tile class.
// Represents a WordGrid tile.
[<AllowNullLiteral>]
type Tile(letter : Letter, blankLetter : Letter option) =
// For a played tile, the second parameter can be a letter when the first parameter is Letter.Blank
static let letterChar =
[| '_'; 'A'; 'B'; 'C'; 'D'; 'E'; 'F'; 'G'; 'H'; 'I'; 'J'; 'K'; 'L'; 'M'; 'N'; 'O'; 'P'; 'Q'; 'R'; 'S'; 'T'; 'U'; 'V'; 'W'; 'X'; 'Y'; 'Z' |]
static let pointValues =
[| 0; 1; 3; 4; 2; 1; 4; 2; 4; 1; 8; 5; 1; 3; 1; 1; 3; 10; 1; 1; 1; 1; 5; 4; 8; 4; 10 |]
let letterValue = letter
let pointValue =
pointValues.[(int) letterValue]
member this.LetterChar = letterChar.[int letterValue]
member this.LetterValue = letterValue
member this.PointValue = pointValue
member val BlankLetter : Letter option = blankLetter with get, set
static member FromString(tiles : string) =
Seq.map(fun (elem : char) -> new Tile(elem)) tiles
|> Seq.toList
new(letter : Letter) =
new Tile(letter, None)
new(ch : char) =
if (ch = '_') then Tile(Letter.Blank) else
if (ch >= 'a' && ch <= 'z') then
// This represents a blank tile that has been played as a letter.
Tile( Letter.Blank, Some(enum<Letter> (int ch - int 'a' + 1 )))
else
Tile( enum<Letter> (int ch - int 'A' + 1))
In this code:
The attribute AllowNullLiteral is used on the Tile type. You'll find it useful to store Tile objects when you interoperate with C# and use the .NET classes for generic collections.
In F#, the arguments for the primary constructor appear after the class name. In this case, you create a tile by specifying a Letter value, which is an enumeration of possible tile types, including all of the alphabetic characters and the blank. In this word game, players can use a blank tile to represent any letter. When this tile is in a player’s tile rack, the tile doesn’t have any letter value. When a player puts the tile on the game board, the tile gets a specific letter value. The F# option type is used to model this behavior, but the option type isn’t as easy to use from C#, because the option type shows up as an instance of FSharpOption in C#. The next bullet point contains a solution to this issue.
The class also includes additional constructors, which use the new keyword. These constructors must call the primary constructor, but they can also perform additional computations. The constructor that takes a Letter as a single parameter just allows you to omit the parameter that handles blanks. By using this constructor, you don't have to use the option type from C#. The constructor that takes a character is called from the FromString static method, which supports the representation of the tiles or game board as a string that can be stored in the database. In this encoding, a capital letter indicates an ordinary letter tile, a lowercase letter represents a blank that has been played as a particular letter, and the underscore character represents an unplayed blank tile.
The class uses static and non-static fields, which are private, and exposes public properties for obtaining the letter value and the point value of the tile.
You use the keywords member val to define BlankLetter as an automatic property in F#. The BlankLetter property represents the letter that any blank tile was given during play.
The Player class is a typical class that mirrors database data. The following code shows the Player class, with some functions omitted for brevity.
// Represents a player in an active game and manages the state for that player,
// including the score and tile rack.
type Player(userId, gameId, playerId, name, score, tiles) =
// The userId that the player uses to log in.
member val UserId = userId
// The database Id of the game
member val GameId = gameId
// The database ID of the player
member val PlayerId = playerId
// The list of tiles in the player's rack
member val Tiles = tiles with get, set
// The player's score.
member val Score = score with get, set
// The name of the player.
member val Name = name
// Returns the tiles as a System.Collections.Generic.List instead of an F# list.
member this.GetTilesAsList() =
let returnList = new System.Collections.Generic.List<Tile>()
for tile in this.Tiles do
returnList.Add(tile);
returnList
// Sets the player's tiles from a System.Collections.Generic.List.
member this.TilesFromList(tileList : System.Collections.Generic.List<_>) =
this.Tiles <- List.ofSeq tileList
// This constructor is used when existing players start a game
.
new(gameId, playerId) =
// Get the player from the database
let dataContext = sqldata.GetDataContext()
let player =
query {
for player in dataContext.Players do
where (player.Id = playerId)
select player
}
|> Seq.exactlyOne
let playerStateResults =
query {
for playerState in dataContext.PlayerState do
where (playerState.GameID = gameId && playerState.PlayerID = playerId)
select playerState
}
|> Seq.toList
match playerStateResults with
| [] -> Player(player.UserId, gameId, player.Id, player.UserName, 0, List.empty)
| [ playerState ] -> Player(player.UserId, gameId, player.Id, player.UserName, playerState.Score, Tile.FromString (playerState.Tiles))
| _ -> raise (new System.InvalidOperationException()); Player(0, 0, 0, "", 0, List.empty)
// This constructor is used when a new player starts a game. If the player has
// played a previous game, the database contains a record for them. If not, a new one is created.
static member FindOrCreate(userId, userName) : Player =
// Look up the player, and create a record if the player doesn't already exist in the database.
// Get the player from the database
let dataContext = sqldata.GetDataContext()
let players =
query {
for player in dataContext.Players do
where (player.UserId = userId && player.UserName = userName)
select player
}
|> Seq.toList
match players with
| [] -> // no player was found, so create
let sqlText = System.String.Format("INSERT INTO Players VALUES ('{0}', '{1}')", userName, userId)
dataContext.DataContext.ExecuteCommand(sqlText) |> ignore
Player.FindOrCreate(userId, userName)
| [ player ] -> // one player was found
Player(userId, 0, player.Id, userName, 0, List.empty)
| _ -> // multiple players found: error case
raise (new System.Exception("Duplicate player found."))
// Gets all the games that the player is currently playing by their IDs.
member this.GetGameIDs(myTurnOnly : bool) =
let dataContext = sqldata.GetDataContext()
if (myTurnOnly) then
query {
for game in dataContext.Games do
join gamePlayer in dataContext.GamePlayers on (game.Id = gamePlayer.GameId)
where (game.GameState <> int GameState.GameOver &&
gamePlayer.PlayerId = this.PlayerId &&
gamePlayer.Position =? game.CurrentPlayerPosition)
select (game.Id)
}
|> Seq.toArray
else
query {
for game in dataContext.Games do
join gamePlayer in dataContext.GamePlayers on (game.Id = gamePlayer.GameId)
where (game.GameState <> int GameState.GameOver &&
gamePlayer.PlayerId = this.PlayerId )
select (game.Id)
}
|> Seq.toArray
Instances of the Player class are created each time a WordGrid web page is requested. This behavior is because this game is implemented by a web application that uses the REST (Representational State Transfer) model. REST is a design model, not a technology. REST-based design includes the principle that each web request (such as a player viewing a particular game or submitting a move) is independent, and the server doesn't keep any state information. Therefore, all objects are re-created for each new request.
Notice the following about the preceding code:
The GetTilesAsList method shows the conversion of an F# list into a List<T>. This type of conversion is used here to avoid having to work with the F# list type in C#. For long lists where performance is an issue, you might want to use the C# list in the F# code to avoid traversing the list more than necessary.
The database code in the WordGrid game is all based on the SqlDataConnection type provider, which was created by using just one line of code at the top of the WordGrid.fs code file.
type sqldata = SqlDataConnection<ConnectionStringName = "DefaultConnection", ConfigFile = @"Web.config">
This single line of code instructs the F# compiler to generate a set of types that represent database objects. The database is referenced by the connection string that’s stored as DefaultConnection in the Web.config file for the project. Elsewhere in this file, you only need to access the DataContext property of the sqldata type provider to access any of the type provider’s generated types, such as Players and Games. The types of these properties are nested, they're generated from database tables of the same name, and their instances are collections of rows from those tables. The DataContext property provides an instance of a generated class that's derived from DataContext.
You use query expressions to access the database tables. In the GetGameIDs method (and in the GetGameNamesmethod, which isn’t shown), join joins the Game and GamePlayers tables.
In the FindOrCreate method, the ExecuteCommand method runs a constructed Transact-SQL command to insert a new record for the player in the database.
If you're starting to learn F#, note the use of the match expression to conveniently branch the code based on whether the players collection has 0, 1, or more elements. See Match Expressions (F#).
The next type in WordGrid.fs is the game board, which this topic doesn't include because it's long and doesn’t show any additional concepts.
The next type in WordGrid.fs is the Move type. This type represents a single play, that is, the placement of one or more tile on the board in particular rows and columns to form one or more words. This type includes the tiles that were played, the location of the tiles on the board, the direction of play, and the tiles that remain on the player’s rack. These properties are all automatic properties.
// Represents the play for a single turn in a WordGrid game.
type Move(gameId : int, playerId : int, row : int, col : int,
dir : Direction, tiles : string, remainingTiles : string) =
// The id from the database for the game in which this move was played.
member val GameID = gameId
// The player who's making this move.
member val PlayerID = playerId
// The row of the first tile that's played in this move.
member val Row = row
// The col of the first tile that's played in this move.
member val Col = col
// The direction of the move.
member val Direction = dir
// The tiles that were played for this move, not including any previously
// played tiles that are part of the word or words that the move formed.
member val Tiles = Tile.FromString(tiles)
// The tiles that remain in the user's rack after the move.
member val RemainingTiles = Tile.FromString(remainingTiles)
Information about the move is composed on the player's client, in the browser. JavaScript in the web application allows the player to position tiles on the board, so that’s not part of the F# code at all. When a player has specified a move and chosen the Submit Play button, a form is submitted as an HTTP POST request. A controller, called Board in the ASP.NET application, reads the form values that are submitted and generates a Move object, which is then submitted to the Game class. The Game class includes logic for starting a game, loading the game from the database, processing moves, and determining when the game finishes. This class is also long, so this topic highlights a few key aspects instead of including the full code. You should review the full code in the sample to understand how it all fits together.
In the ProcessPlay method, code shows how to call stored procedures by using the type provider.
// Update the database tables: Games, PlayerState, Plays
Game.DataContext.SpUpdateGame(Util.nullable this.GameId,
Util.nullable player.PlayerId,
Util.nullable this.MoveCount,
Game.AsString player.Tiles,
mainWord,
gameBoard.AsString,
Game.AsString this.TileBag,
Util.nullable player.Score,
Util.nullable (int this.State),
Util.nullable ((this.CurrentPlayerPosition + 1) % players.Length))
This code runs the stored procedure spUpdateGame from the database. The F# type provider creates a method on the DataContext-derived class for each stored procedure.
CREATE PROCEDURE [dbo].spUpdateGame
@gameId int,
@playerId int,
@moveNumber int,
@playerTiles varchar(50),
@wordPlayed varchar(50),
@boardLayout varchar(MAX),
@tileBag varchar(MAX),
@score int,
@gameState int,
@currentPlayerPosition int
AS
INSERT INTO Plays
VALUES(@gameId, @wordPlayed, @moveNumber, @score, @playerId, GETDATE())
UPDATE PlayerState
SET PlayerState.Score = @score,
PlayerState.Tiles = @playerTiles
WHERE PlayerState.GameID = @gameid AND PlayerState.PlayerID = @PlayerId
UPDATE Games
SET Games.GameState = @gameState,
Games.BoardLayout = @boardLayout,
Games.Tilebag = @tileBag,
Games.CurrentPlayerPosition = @currentPlayerPosition
WHERE Games.Id = @gameId
RETURN 0
The F# code relies on a helper routine that converts a value to a nullable value. The stored procedure has some parameters that allow null values. Therefore, you must convert the values to Nullable<T> before passing them as arguments. The following conversion function, nullable, is required and part of a module Util.
// Utility functions.
module Util =
let nullable value =
new System.Nullable<_>(value)
In F#, you use an underscore in place of a generic parameter when you want the parameter to be inferred. In this case, the generic parameter is inferred from the type of the value that's passed in.
The WordGrid Game class uses a Windows Azure queue to communicate with a worker role, which checks the words that were played in a given move and then posts a message to another queue with the results. In a real-world application, you could just create a local library to look up the words in process, instead of invoking another worker role. For illustrative purposes, this sample shows the basic use of Windows Azure queues for communicating between roles in a Windows Azure cloud service. You could also explore the use of Service Bus queues instead of Windows Azure queues for this scenario.
The following code defines and creates the two queues.
do CloudStorageAccount.SetConfigurationSettingPublisher(new System.Action<_, _>(fun configName configSetter ->
// Provide the configSetter with the initial value.
configSetter.Invoke( RoleEnvironment.GetConfigurationSettingValue( configName ) ) |> ignore
RoleEnvironment.Changed.AddHandler( new System.EventHandler<_>(fun sender arg ->
arg.Changes
|> Seq.toList
|> List.filter (fun change -> change :? RoleEnvironmentConfigurationSettingChange)
|> List.map (fun change -> change :?> RoleEnvironmentConfigurationSettingChange)
|> List.filter (fun change -> change.ConfigurationSettingName = configName &&
not (configSetter.Invoke( RoleEnvironment.GetConfigurationSettingValue(configName))))
|> List.iter (fun change ->
// In this case, the change to the storage account credentials in the
// service configuration is significant enough that the role must be
// recycled to use the current settings. (For example, the
// endpoint may have changed.)
RoleEnvironment.RequestRecycle())))))
let storageAccount = CloudStorageAccount.FromConfigurationSetting("StorageConnectionString")
let queueClient = storageAccount.CreateCloudQueueClient()
let lookupQueue = queueClient.GetQueueReference("lookup")
let resultsQueue = queueClient.GetQueueReference("results")
do
lookupQueue.CreateIfNotExist () |> ignore
resultsQueue.CreateIfNotExist () |> ignore
Finally, the following code sends a message and finds its response.
member this.TryFindResponse(messageHeader) =
async {
let! results = resultsQueue.GetMessagesAsync(32)
let response = results
|> Seq.tryPick (fun message ->
let results = message.AsString
let split = results.Split([|'\n'|])
if (split.[0] = messageHeader) then
Async.Start <| resultsQueue.DeleteMessageAsync(message.Id, message.PopReceipt)
let result = split |> Array.toList |> List.tail
Some(result)
else
None
)
return response
}
// In a dictionary, look up all the words that were played in one move.
// Return a list of tuples, with the word and whether it was found.
member this.CheckWordsAsync words =
async {
let now = System.DateTime.Now
let messageHeader = now.ToLongTimeString()
+ " " + now.ToLongDateString()
+ " " + this.Players.[this.CurrentPlayerPosition].PlayerId.ToString()
+ " " + this.GameId.ToString()
let message = new CloudQueueMessage(String.concat "\n" (messageHeader :: words))
do! lookupQueue.AddMessageAsync(message)
let rec findResponse messageHeader n =
async {
let! response = this.TryFindResponse(messageHeader)
let result =
match response with
| Some value -> async { return value }
| None -> findResponse messageHeader (n + 1)
return! result
}
let! result = findResponse messageHeader 0
return result
|> List.map (fun (elem : string)-> elem.Split([|' '|]))
|> List.map( fun (elem :string []) -> (elem.[0], elem.[1].ToLower() = "true"))
}
The previous code runs in the context of an HTTP form submission and a full round-trip. In other words, a message and its response is required before the HTTP response is given to the client. Therefore, the thread that runs this code is part of an ASP.NET thread pool. A thread pool has a limited number of threads. If thousands of users play this game simultaneously, many requests to look up words might arrive in a short amount of time. If looking up words is a lengthy process, the asynchronous calls to the messaging APIs are needed so that the thread can be suspended and other requests from other players can be processed.
To support asynchronous operations, the API for the storage services queue in Windows Azure uses the Begin/End pattern. In this legacy model for asynchronous programming, you must call a separate Begin and End method for each asynchronous queue operation. For example, you add a message by first calling BeginAddMessage then calling EndAddMessage to retrieve the result when it's ready. In F#, you can wrap these methods by using the Async.FromBeginEnd method to call a single asynchronous extension method. You create these wrappers as the following excerpt shows. The excerpt is from the file AzureHelper.fs in the FsAzureHelper project.
// Contains types and functions that support the use of Windows Azure,
// including asynchronous methods for queue operations.
module FsAzureHelpers =
// Extension methods to support asynchronous queue operations.
type CloudQueue with
member this.AddMessageAsync(message) =
Async.FromBeginEnd(message, (fun (message, callback, state) ->
this.BeginAddMessage (message, callback, state)),
this.EndAddMessage)
member this.ClearAsync() =
Async.FromBeginEnd((fun (callback, state) -> this.BeginClear(callback, state)),
this.EndClear)
member this.CreateAsync() =
Async.FromBeginEnd((fun (callback, state) -> this.BeginCreate(callback, state)),
this.EndCreate)
...
See Asynchronous Workflows (F#).
One of the drawbacks of using Windows Azure storage services queues, rather than Service Bus queues, is that you must find the correct return message by searching in each message in turn. A message header is created that must be searched for in the TryFindResponse method. Messages are retrieved 32 at a time, and the headers are scanned to find the correct message. For more information about queues in Windows Azure, see How to use the Queue Storage Service.
Overview of the C# ASP.NET Code
The C# ASP.NET code consists of models, views, and controllers. The model types represent the abstract data behind what will appear in the user interface. The project MvcWebRole1 and the file GameModels.cs, which is in the Models folder, contain types for PlayerModel, BoardCell, BoardRow, TileRack, and GameModel. In some cases, these types wrap F# types. For example, the PlayerModel type wraps the Player class, and the GameModel class wraps the F# Game type. In other cases, the mapping isn’t direct. BoardCell and BoardRow cooperate to represent the game board, but F# contains only a GameBoard class. The C# model class TileRack represents the player’s tiles in addition to the area on the web page that displays those tiles.
First, you can examine the PlayerModel class.
// Represents a player in a WordGrid game.
public class PlayerModel
{
Player player;
public int PlayerID { get { return player.PlayerId; } }
public UserProfile Profile;
public PlayerModel(UserProfile userProfile)
{
Profile = userProfile;
player = Player.FindOrCreate(userProfile.UserId, userProfile.UserName);
}
public int[] GetAllGameIDs()
{
return player.GetGameIDs(false);
}
public string[] GetAllGameNames()
{
return player.GetGameNames(false);
}
public int[] GetMyTurnGameIDs()
{
return player.GetGameIDs(true);
}
public string[] GetMyTurnGameNames()
{
return player.GetGameNames(true);
}
}
This class is a relatively thin wrapper for the F# Player class but also contains the UserProfile. The UserProfile class is an ASP.NET class that ties this player to a user with a username and password in the ASP.NET SimpleMembership database. Its definition can be found in the AccountModel.cs file in the Models folder of the MvcWebRole1 project.
The following class, TileRack, is a fairly straightforward representation of tiles that a player could play on the board.
// Represents the tile rack in a Word Grid game.
public class TileRack
{
Tile[] tilesInRack = new Tile[Constants.MaxTilesInRack];
public TileRack(List<Tile> tiles)
{
var count = 0;
foreach (Tile tile in tiles)
{
tilesInRack[count++] = tile;
}
}
public Tile[] Tiles
{
get { return tilesInRack; }
}
public void UpdateRack(List<Tile> tiles)
{
var count = 0;
foreach (Tile tile in tiles)
{
tilesInRack[count++] = tile;
}
}
}
The underlying F# type Tile is used for individual tiles in the rack, and the collection that represents the tiles is a List<T>, not an F# list, thanks to the conversion function referenced earlier.
The BoardCell class contains information about the images that will represent the tiles for different types of squares (double letter spaces, for example).
The GameModel class wraps the F# Game type and brings together references to the board and the tile rack, in addition to information about the player. The following code shows the GameModel class, with some properties and methods omitted.
// Represents a player's WordGrid session, the game that they're currently
// playing, and all its elements.
public class GameModel
{
Game game;
BoardRow[] rows;
TileRack rack;
Player currentPlayer;
UserProfile currentUser;
int userPlayerId;
// From a Game object and the current user, construct the information that's needed
// to render a particular play layout for a user's game.
public GameModel(Game gameIn, UserProfile currentUserIn)
{
game = gameIn;
currentUser = currentUserIn;
rows = new BoardRow[Constants.BoardSize];
for (int row = 0; row < Constants.BoardSize; row++)
{
rows[row] = new BoardRow(game, row);
}
// Identify the Player object and the
// playerId of the current user
int playerId = 0;
bool found = false;
int index = 0;
int indexOfCurrentPlayer = 0;
while (! found)
{
Player player = game.Players[index];
if (player.UserId == currentUser.UserId)
{
playerId = player.PlayerId;
indexOfCurrentPlayer = index;
found = true;
}
index++;
}
if (!found)
throw new InvalidOperationException();
currentPlayer = game.Players[indexOfCurrentPlayer];
userPlayerId = playerId;
List<Tile> tiles = currentPlayer.GetTilesAsList();
rack = new TileRack(tiles);
}
// Create a game given an array of user profiles who'll
// play.
public static GameModel NewGame(UserProfile[] playerProfiles)
{
Player[] players = new Player[2];
int count = 0;
foreach (UserProfile profile in playerProfiles)
{
players[count++] = Player.FindOrCreate(profile.UserId, profile.UserName);
}
Game game = new Game(players);
game.StartGame();
return new GameModel(game, playerProfiles[0]);
}
// Retrieves a game that's already in progress.
public static GameModel GetByID(int id, UserProfile currentUser)
{
Game game = new Game(id);
return new GameModel(game, currentUser);
}
// Tries to play a move. Processes the move that the client sent.
public string PlayMove(Move move)
{
string userMessage = game.ProcessMove(userPlayerId, move);
rack.UpdateRack(game.GetPlayerById(userPlayerId).GetTilesAsList());
return userMessage;
}
The views use the preceding model classes to form the web pages that players see. The views were authored in the Razor view engine, which has its own syntax and structure and uses the file extension .cshtml. By using the Razor view engine, you can write an HTML webpage and include C# code inline as needed to generate portions of the pages. In our examples, C# code in the Razor views displays the board and the tile rack by using the data from the relevant C# model classes. The top of the view shows the reference to the model that contains the data that the view uses. The Razor view engine interprets any syntax that's marked by the "at" symbol (@). Everything else is HTML. See Introduction to ASP.NET Web Programming Using the Razor Syntax.
The following code shows the view for the main game board, in the file Play.cshtml in the Views subfolder of the MvcWebRole1 project. Some of the page layout is omitted.
@model MvcWebRole1.Models.GameModel
@using MvcWebRole1.Models
@using WordGridGame
@{
ViewBag.Title = "Board";
Layout = "~/Views/Shared/_LayoutBoard.cshtml";
}
<script src="@Url.Content("~/Scripts/WordGridScript.js")" type="text/javascript"></script>
<!-- The game board consists of a series of rows and a series of cells in the rows.
Each cell is either occupied or unoccupied and, if occupied, contains a tile.
-->
<style>
...
</style>
@using (@Html.BeginForm("Move", null, FormMethod.Post, new { name = "wordgrid", onsubmit = "return capturePlayDetails(event)" }))
{
@Html.AntiForgeryToken()
<h2>WordGrid</h2>
<p id="userMessage">@Model.UserMessage</p>
<table id="board" class="board">
@foreach (BoardRow row in Model.Rows)
{
<tr class="row">
@{
foreach (BoardCell cell in row.Cells)
{
if (cell.IsEmpty)
{
<td class="cell"> <div class="boardSpace" ondrop="dropItem(event)" ondragover="allowDrop(event)" id="@cell.CellID" onclick="OnClick(event)">
<img id="img@(cell.CellID)" class="@cell.ClassName" alt="@((int)cell.Space)" src="@cell.ImagePath" width="24" /></div>
</td>
}
else
{
<td class="cell"> <div class="boardSpace" ondrop="dropItem(event)" ondragover="allowDrop(event)" id="@cell.CellID" onclick="OnClick(event)">
<img id="img@(cell.CellID)" class="@cell.ClassName" alt="@cell.Letter@cell.PointValue" src="@cell.ImagePath" width="24" /></div>
</td>
}
}
}
</tr>
}
</table>
<input name="row" id="row" type="hidden" value="" />
<input name="col" id="col" type="hidden" value="" />
<!-- <input name="word" type="hidden" value="" />-->
<input name="direction" id="direction" type="hidden" value="" />
<input name="gameID" id="gameID" type="hidden" value="@Model.GameID" />
<input name="playerID" id="playerID" type="hidden" value="@Model.UserPlayerID" />
<input name="tiles" id="tiles" type="hidden" value="" />
<input name="remainingTiles" id="remainingTiles" type="hidden" value="" />
<input type="submit" id="submitPlay" value="Submit Play" disabled="disabled" />
if (Model.IsUsersTurn)
{
<input type="button" id="swapTiles" value="Swap Tiles" onclick="swapTiles()" />
}
}
The following illustration shows the web page that this view produces.
ASP.NET compiles this code into a C# class that generates an HTML stream when the view is invoked by a controller in response to HTML requests. Notice the following about this code.
Some important global values are set in the section at the top. For example, ViewBag.Title sets the page title.
The code refers to a layout view in the following line.
Layout = "~/Views/Shared/_LayoutBoard.cshtml"
The _LayoutBoard.cshtml file contains a view that includes a master layout of pages on the site. If multiple pages use the same layout, you don’t have to repeat all of it in the view for each page.
The script tag contains a reference to a JavaScript file in the Scripts folder of the same project. This script file contains all the client-side JavaScript logic, including code that allows players to drag tiles between the rack and board, scoring moves during play, and determining whether a move is valid.
The following line of code, using @Html.BeginForm, starts the main form, which allows players to submit moves as HTTP post requests.
@using (@Html.BeginForm("Move", null, FormMethod.Post, new { name = "wordgrid", onsubmit = "return capturePlayDetails(event)" }))
The anonymous type in the last parameter specifies attributes for the form, including the reference to the capturePlayDetails JavaScript function. This function analyzes play information and sets the values of hidden elements (that is, input elements whose type is set to hidden) on the page that encode the player’s move.
The reference to @Html.AntiForgeryToken is a security measure that you should use on form submissions to prevent certain types of attacks.
The following syntax injects values into the HTML stream by invoking the model (in this case, the GameModel class).
<p id="userMessage">@Model.UserMessage</p>
This syntax works because UserMessage is a property on the GameModel class.
The code uses the Razor syntax @foreach to loop through the rows of the board. Also, to navigate the cells in each row, C# code appears within braces @{ and }.
Within C# code blocks, HTML elements appear and are emitted literally. Within HTML, the code references variable values from C#, again by using @, as the following code shows.
<img id="img@(cell.CellID)" class="@cell.ClassName" alt="@((int)cell.Space)" src="@cell.ImagePath" width="24" />
The player chooses the Submit Play button to submit a move. This element calls the submitPlay JavaScript function, as the following code shows.
<input type="submit" id="submitPlay" value="Submit Play" disabled="disabled" />
The Submit Play button is enabled only when the user has placed tiles on the board that make up a potentially valid play. For example, two tiles placed on the board that don’t connect or that don’t fall into a single row or column won’t be validated as a legal move. JavaScript functions are called to check the validity of a play whenever a player moves tiles. This action differs from validating whether a given move makes legal words, which is done on the server.
Players who can’t form a word can choose the Swap Tiles button, which returns tiles to the tile bag, the repository of unused tiles, and draws the same number of tiles back.
The file BoardController.cs contains the BoardController class, which includes methods for handling different types of requests that occur on the game board page. HTML GET requests occur whenever a player views a game in progress. HTML POST requests occur when a player submits a move for consideration.
// This class accepts incoming HTTP requests and
// determines the appropriate action, which could
// be a Word Grid move (see Move and Play), swapping tiles (see Swap),
// or creating a game (see NewGame and CreateGame).
[ValidateInput(true)]
public class BoardController : Controller
{
int x;
GameModel gameModel;
// GET: Board/Play
// Handles the display of the main game board when a turn starts.
public ActionResult Play(int gameID, string userMessage)
{
var currentUser = GetCurrentUserProfile();
gameModel = GameModel.GetByID(gameID, currentUser);
gameModel.UserMessage = userMessage;
ViewBag.Title = "Word Grid";
return View(gameModel);
}
// POST: Board/Move
// Handles the main game board's form submission, which occurs
// when a player submits a move to the server.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Move(FormCollection formValues)
{
int gameId = Int32.Parse( formValues["gameId"]);
int playerId = Int32.Parse( formValues["playerId"]);
int row = Int32.Parse( formValues["row"]);
int col = Int32.Parse( formValues["col"]);
int direction = Int32.Parse(formValues["direction"]);
string tiles = formValues["tiles"];
string remainingTiles = formValues["remainingTiles"];
WordGridGame.Direction directionOfPlay;
switch (direction)
{
case 0: directionOfPlay = WordGridGame.Direction.Across;
break;
case 1: directionOfPlay = WordGridGame.Direction.Down;
break;
case 2: directionOfPlay = WordGridGame.Direction.Single;
break;
default: directionOfPlay = WordGridGame.Direction.Across;
break;
}
var move = new WordGridGame.Move(gameId, playerId, row, col, directionOfPlay, tiles, remainingTiles);
var user = GetCurrentUserProfile();
gameModel = GameModel.GetByID(move.GameID, user);
string userMessage = gameModel.PlayMove(move);
return RedirectToAction("Play", new { gameId, userMessage });
}
// GET: Board/Swap
// Processes a tile swap move.
[HttpGet]
public ActionResult Swap(string gameId, string playerId, string tilesToSwap)
{
var user = GetCurrentUserProfile();
gameModel = GameModel.GetByID(Int32.Parse(gameId), user);
if (gameModel.UserPlayerID == Int32.Parse(playerId))
{
gameModel.SwapTiles(tilesToSwap);
}
string userMessage = "You've swapped tiles.";
return RedirectToAction("Play", new { gameId, userMessage });
}
Notice the following about the preceding code:
ValidateInputAttribute is set to true as a security measure, to prevent attacks where a malicious user attempts to insert HTML where plaintext is expected.
The methods of a controller class respond to specific types of URLs. The name of the controller class makes up part of the URL, so BoardController handles requests that have Board in the URL, and the Play method handles URLs of the form Board/Play. The arguments of the Play controller method are derived from URL parameters, gameID and userMessage. Omitted parameters result in default values. The following URL would result in a call to Play with gameID equal to 13 and userMessage equal to an empty string:
http://sitename.cloudapp.net/Board/Play?gameID=13
The ASP.NET engine handles the processing of URL parameters and determines which method matches the URL that was passed.
The Move method uses a FormCollection as a parameter. The required data is submitted as values of form elements, which can be extracted from a FormCollection. You could also use an object type as the parameter and have the form values wrapped up automatically.
Overview of the F# Worker Role
The F# worker role looks up words in the dictionary. This part of the sample requires a dictionary file, which the sample doesn't include, in the form of a text file. You can use any one of several online dictionary files, with varying copyrights and licensing agreements. You can even use the Word spelling checker if you installed it on your server.
The worker role monitors a queue of requests for words to look up. Each message in the queue corresponds to a single play, although each play can contain multiple words because players can potentially form many different crosswords in a single move. A response consists of a message with the candidate words and a true or false value that indicates whether the word was in the dictionary.
The following code implements the worker role.
module DictionaryInfo =
let dictionaryFilename = "dictionary.txt"
#if USE_MSWORD_DICTIONARY
// Includes functionality for looking up words in the Word
// spelling dictionary.
type WordLookup() =
inherit obj()
let app = new Microsoft.Office.Interop.Word.ApplicationClass()
do app.Visible <- false
do app.AutoCorrect.ReplaceText <- false
do app.AutoCorrect.ReplaceTextFromSpellingChecker <- false
let refTrue = ref (box true)
let refFalse = ref (box false)
// Determines whether a list of words is valid according to the
// Word spelling dictionary.
member this.CheckWords(words) =
// Change to lowercase here because proper nouns will be reported as
// misspelled if they're lowercase.
let words = List.map (fun (word : string) -> word.ToLower()) words
let doc1 = app.Documents.Add(Visible = refFalse)
let text = String.concat(" ") words
doc1.Words.First.InsertBefore(text)
let errors = doc1.SpellingErrors
let numError = errors.Count
let invalidWords = seq { for error in errors do
yield error.Text
}
|> List.ofSeq
doc1.Close(SaveChanges = refFalse)
let isValid word = not (List.exists (fun element -> element = word) invalidWords)
List.map (fun element -> isValid element) words
|> List.zip (List.map (fun (word: string) -> word.ToUpper()) words)
// Close the instance of Microsoft Word
override this.Finalize() =
app.Quit(refFalse)
base.Finalize()
#endif
// This worker role looks words in the dictionary. When the user makes a move,
// a message is submitted to a queue "lookup," which contains the words to look up for that
// move. The role looks up the words and submits a message back to the "results" queue, which
// contains the information about whether each word is valid.
type WorkerRole() =
inherit RoleEntryPoint()
let log message kind = Trace.WriteLine(message, kind)
do CloudStorageAccount.SetConfigurationSettingPublisher(new System.Action<_, _>(fun configName configSetter ->
// Provide the configSetter with the initial value.
configSetter.Invoke( RoleEnvironment.GetConfigurationSettingValue( configName ) ) |> ignore
RoleEnvironment.Changed.AddHandler( new System.EventHandler<_>(fun sender arg ->
arg.Changes
|> Seq.toList
|> List.filter (fun change -> change :? RoleEnvironmentConfigurationSettingChange)
|> List.map (fun change -> change :?> RoleEnvironmentConfigurationSettingChange)
|> List.filter (fun change -> change.ConfigurationSettingName = configName &&
not (configSetter.Invoke( RoleEnvironment.GetConfigurationSettingValue(configName))))
|> List.iter (fun change ->
// In this case, the change to the storage account credentials in the
// service configuration is significant enough that the role must be
// recycled to use the most recent settings. (For example, the
// endpoint may have changed.)
RoleEnvironment.RequestRecycle())))))
let storageAccount = CloudStorageAccount.FromConfigurationSetting("StorageConnectionString")
let blobClient = storageAccount.CreateCloudBlobClient()
let blobContainer = blobClient.GetContainerReference("WordGrid")
let queueClient = storageAccount.CreateCloudQueueClient()
#if USE_MSWORD_DICTIONARY
let wordLookup = new WordLookup()
#endif
// An in-memory representation of the word list that's used in this game.
let dictionary =
let blob = blobContainer.GetBlobReference(DictionaryInfo.dictionaryFilename)
let blobString = blob.DownloadText()
blobString.Split([|'\r'; '\n'|], StringSplitOptions.RemoveEmptyEntries)
// Looks up a single word in the dictionary.
member this.IsValidWord(word) =
Seq.exists (fun elem -> elem = word) dictionary
// Verify the words that were played in a move.
member this.CheckWords(words) =
#if USE_MSWORD_DICTIONARY
wordLookup.CheckWords()
#else
List.map (fun word -> this.IsValidWord(word)) words
|> List.zip words
#endif
// This is the main message processing loop for the F# worker role.
override this.Run() =
log "WorkerRole1 entry point called" "Information"
let lookupQueue = queueClient.GetQueueReference("lookup")
let resultQueue = queueClient.GetQueueReference("results")
lookupQueue.CreateIfNotExist() |> ignore
resultQueue.CreateIfNotExist() |> ignore
while(true) do
let message = lookupQueue.GetMessage()
if (message = null) then
Thread.Sleep(1000)
else
lookupQueue.DeleteMessage(message)
// Messages contain a token (ID) on the first line and
// then a list of words to look up for a move, one word per line.
// The return message contains the same token (ID) and
// the list of words with a Boolean IsValid value for each word.
let messageString = message.AsString
let buildMessage id checkResults =
let body = List.map (fun (word, isValid) -> String.Format("{0} {1}", word, isValid)) checkResults
|> String.concat "\n"
String.Concat(id + "\n", body)
let newMessage =
match (messageString.Split([| '\n' |]) |> Array.toList) with
| head :: tail -> new CloudQueueMessage(buildMessage head (this.CheckWords(tail)))
| [] -> new CloudQueueMessage("Unknown error in parsing a message in the lookup queue.")
resultQueue.AddMessage(newMessage)
log "Working" "Information"
// Perform any initial configuration on startup.
override this.OnStart() =
// Set the maximum number of concurrent connections.
ServicePointManager.DefaultConnectionLimit <- 12
base.OnStart()
Notice the following about the preceding code:
The basic pattern for a worker role involves a WorkerRole class that inherits from RoleEntryPoint, which contains a Run method and an OnStart method.
The Run method takes a single message from the queue, looks up the words, and sends a response message.
In this implementation, messages are processed synchronously. On a machine that has multiple cores, you might want to process messages asynchronously on different threads.
The Word spelling checker is relatively easy to use through the Word object model and the interop assembly. To enable the use of that dictionary, add the following line of code to the beginning of the file WorkerRole.fs.
#define USE_MSWORD_DICTIONARY
The Word spelling checker includes many words that aren’t traditionally legal in these types of word games. See Word Object Model Overview.
Next Steps
Try to extend and improve the code. You could perform the following relatively short coding projects with this sample.
Instead of Windows Azure queues, use a Service Bus queue.
Implement a feature that's common to online word games but that isn’t implemented, such as the ability to recall tiles all at once back to the rack, shuffle tiles in the rack, display game history, or forfeit a game.
Look up published algorithms for crossword game AI, and create an F# worker role to implement it. Implement alternatives for playing tiles (other than dragging), so that players can play WordGrid on more kinds of devices.
See Also
Tasks
Walkthrough: Publishing an F#/C# MVC Application to Windows Azure