Click here to Skip to main content
15,867,308 members
Articles / Mobile Apps

Windows Phone Crosswords

Rate me:
Please Sign up or sign in to vote.
5.00/5 (22 votes)
3 Jul 2012CPOL6 min read 61.5K   1.4K   42   17
Learn how to create a Windows Phone crosswords game taking advantage of online internet resources

Image 1

Table of Contents

Introduction

In this article, I'll explain how to create a new game, a newspaper-style crossword puzzle, by using the Windows Phone framework to access resources existing on the internet.

The internet is full of useful and free services for a myriad of areas, and we just need to find out which services fit our need. In our case, we needed some online English dictionary to provide the clues for the crosswords. I hope the following article may be of help for those looking for similar kind of applications.

System Requirements

To use Crosswords app provided with this article, you must have installed the Windows Phone SDK 7.1 that you can download 100% free directly from Microsoft:

Selecting the Puzzle Size

There are 3 different puzzle sizes available: 4 x 4, 7 x 7 and 10 x 10. The user can choose one of them from the main menu, and later the application will both generate a query string for a web request and process the HTML response based on the dimensions selected by the user:

Image 2

When the user selects a different size, the image displayed in the menu changes accordingly, and the new size is stored in a local variable:

C#
private void RadioButton_Click(object sender, RoutedEventArgs e)
{
    if (rbt4x4.IsChecked.Value)
        size = 4;
    else if (rbt7x7.IsChecked.Value)
        size = 7;
    else if (rbt10x10.IsChecked.Value)
        size = 10;

    var canPlay = true;

    btnNewGame.Visibility = canPlay ? Visibility.Visible : Visibility.Collapsed;
    btnPurchase.Visibility = canPlay ? Visibility.Collapsed : Visibility.Visible;

    imgSize.Source = new BitmapImage(new Uri(string.Format(@"Images/{0}x{0}.png",
        size), UriKind.Relative));
}

When the user clicks the "New Game" button, the NavigationService is told to display the Board.xaml page, which in turn accepts the size that was selected previously.

C#
private void btnNewGame_Click(object sender, RoutedEventArgs e)
{
    NavigationService.Navigate(
        new Uri(string.Format("/Board.xaml?StartMode=2&Size={0}", size),
        UriKind.Relative));
}

Downloading the Puzzle HTML

It's important to notice that the crossword puzzle is not generated by this application. I would take a big "word list" and a good algorithm to do that in your app. Instead, we're going to keep our application lean and light, and resort to the internet to do the hard work for us. This means that the puzzle must be generated somewhere else, by some website. The website I chose is from the MIT (Massachusetts Institute of Technology) and provided by Professor Robert Morris.

Professor Morris has provided a very nice online crossword generator, where you can pass the empty cells via query string. The generator will assume that the cells not provided are "blocked" cells, so these particular cells will remain empty.

In our app, we call the crossword generator by providing 3 different kinds of querystrings, being one for each puzzle size (4x4, 7x7 and 10x10):

  1. Puzzle size: 4x4. Request: http://pdos.csail.mit.edu/cgi-bin/theme-cword?r0c0=&r0c1=&r0c2= &r0c3=&r1c0=&r1c1=&r1c2=&r1c3=&r2c0=&r2c1=&r2c2=&r2c3=&r3c0=&r3c1=&r3c2=&r3c3=

    Image 3

  2. Puzzle size: 7x7. Request: http://pdos.csail.mit.edu/cgi-bin/theme-cword?r0c0=&r0c1=&r0c2= &r0c3=&r0c4=&r1c0=&r1c1=&r1c2=&r1c3=&r1c5=&r1c6=&r2c0=&r2c1=&r2c2=&r2c3=&r2c5=&r2c6=&r3c0=&r3c3= &r3c4=&r3c5=&r3c6=&r4c1=&r4c2=&r4c3=&r4c4=&r4c5=&r4c6=&r5c0=&r5c1=&r5c2=&r5c3=&r5c4=&r5c5=&r5c6= &r6c0=&r6c1=&r6c2=&r6c3=&r6c5=&r6c6=

    Image 4

  3. Puzzle size: 10x10. Request: http://pdos.csail.mit.edu/cgi-bin/theme-cword?r0c0=&r0c1= &r0c2=&r0c3=&r0c6=&r0c7=&r0c8=&r1c0= &r1c1=&r1c2=&r1c3=&r1c5=&r1c6=&r1c7=&r1c8=&r2c0=&r2c1=&r2c2=&r2c3=&r2c4=&r2c5=&r2c6=&r2c7=&r2c8= &r2c9=&r3c0=&r3c1=&r3c2=&r3c3=&r3c4=&r3c5=&r3c6=&r3c7=&r3c8=&r3c9=&r4c3=&r4c4=&r4c5=&r4c7=&r4c8= &r4c9=&r5c1=&r5c2=&r5c3=&r5c4=&r5c5=&r5c6=&r5c8=&r5c9=&r6c0=&r6c1=&r6c2=&r6c4=&r6c5=&r6c6=&r6c7= &r7c0=&r7c1=&r7c2=&r7c3=&r7c5=&r7c6=&r7c7=&r7c8=&r7c9=&r8c0=&r8c1=&r8c2=&r8c3=&r8c4=&r8c6=&r8c7= &r8c8=&r8c9=&r9c1=&r9c2=&r9c3=&r9c4=&r9c5=&r9c6=&r9c7=&r9c8=&r9c9=

    Image 5

As you can see in the links above, the querystring can be very long and cumbersome to generate. Instead of just requesting a hard-coded query string, we use a pair of functions to translate a 2D string map (consisting of 0s and 1s, where the zeroes represent the empty cells) into the expected playing querystring:

C#
public class HtmlParser
    {
        .
        .
        .
        public void GetPuzzleHtml(int size, Action<string> onSuccess, 
        Action onFailure, Action<int> onProgressChanged)
        {
            var ret = string.Empty;
            var tileMap = string.Empty;

            switch (size)
            {
                case 4:
                    tileMap = string.Concat(
                        "0000",
                        "0000",
                        "0000",
                        "0000");
                    break;
                case 7:
                    tileMap = string.Concat(
                        "0000011",
                        "0000100",
                        "0000100",
                        "0110000",
                        "1000000",
                        "0000000",
                        "0000100");
                    break;
                case 10:
                    tileMap = string.Concat(
                        "0000110001",
                        "0000100001",
                        "0000000000",
                        "0000000000",
                        "1110001000",
                        "1000000100",
                        "0001000011",
                        "0000100000",
                        "0000010000",
                        "1000000000");
                    break;
            }

            var queryString = GetRequestQueryStringBySize(size, tileMap);
            
            var url = string.Format("http://pdos.csail.mit.edu/cgi-bin/theme-cword{0}", 
            queryString.AppendFormat("&t={0}", DateTime.Now.Millisecond));

            DownloadString
                (url,
                //onSuccess
                (html) =>
                {
                    onSuccess(html);
                },
                //onFailure
                () =>
                {
                    onFailure();
                },
                //progress
                (percentage) =>
                {
                    onProgressChanged(percentage);
                });
        }

Notice the code above shows the asynchronous call, made to the WebRequest class. When the response is ready, we call the action named endGetResponse, which in turn will be responsible for parsing the HTML response (we'll talk in detail about this process later in this article).

The GetRequestQueryStringBySize receives the dimensions of the puzzle and the text map of the puzzle, and generates the request QueryString according to what is expected by the puzzle generator page:

C#
    private static StringBuilder GetRequestQueryStringBySize(int size, string tileMap)
    {
        var queryString = new StringBuilder();
        for (var row = 0; row < size; row++)
        {
            for (var col = 0; col < size; col++)
            {
                var val = tileMap[row * size + col];
                if (val == '0')
                {
                    var prefix = "&";
                    if (string.IsNullOrEmpty(queryString.ToString()))
                    {
                        prefix = "?";
                    }
                    queryString.AppendFormat("{0}r{1}c{2}=", prefix, row, col);
                }
            }
        }
        return queryString;
    }
}

Parsing the Puzzle HTML

The HTML generated by the crossword puzzle generator is quite simple. But we are not really interested in HTML tags, so we must first get rid of all those table, tr and td mark ups, so that we end up with the letters that will make up our crossword:

C#
public class HtmlParser
{
    const string msgFormat = "table[{0}], tr[{1}], td[{2}], code: {3}";
    const string table_pattern = "<table.*?>(.*?)</table>";
    const string tr_pattern = "<tr>(.*?)</tr>";
    const string td_pattern = "<td.*?>(.*?)</td>";
    const string code_pattern = "<code>(.*?)</code>";

    string html = string.Empty;
    WebRequest request;

    public HtmlParser(int size, Action<string> onSuccess,
                      Action onFailure, Action<int> onProgressChanged
        )
    {
        GetPuzzleHtml(size,
            //onSucess
            (html) =>
            {
                this.html = html;
                onSuccess(html);
            },
            //onFailure
            () =>
            {
                onFailure();
            },
            //onProgressChanged
            (percentage) =>
            {
                onProgressChanged(percentage);
            }
        );
    }

    public HtmlParser(string html)
    {
        this.html = html;
    }

    private List<string> GetContents(string input, string pattern)
    {
        MatchCollection matches = Regex.Matches(input, pattern, RegexOptions.Singleline);
        List<string> contents = new List<string>();
        foreach (Match match in matches)
            contents.Add(match.Value);

        return contents;
    }

    public string Parse()
    {
        List<string> tableContents = GetContents(html, table_pattern);
        StringBuilder ret = new StringBuilder();
        int tableIndex = 0;
        foreach (string tableContent in tableContents)
        {
            List<string> trContents = GetContents(tableContent, tr_pattern);
            int trIndex = 0;
            foreach (string trContent in trContents)
            {
                List<string> tdContents = GetContents(trContent, td_pattern);
                int tdIndex = 0;
                foreach (string tdContent in tdContents)
                {
                    Match code_match = Regex.Match(tdContent, code_pattern);
                    string code_value = code_match.Groups[1].Value.Replace(" ", "");

                    if (string.IsNullOrEmpty(code_value))
                        code_value = " ";

                    ret.Append(code_value);
                    tdIndex++;
                }
                ret.Append("");
                trIndex++;
            }
            tableIndex++;
        }

        var words = ret.ToString();

        return words;
    }
    .
    .
    .
}

Notice that the onHtmlReady callback is passed to the HtmlParser class, and is executed once the HTML is ready to be rendered in our application.

C#
public HtmlParser(int size, Action<string> onHtmlReady)
{
    GetPuzzleHtml(size, (html) =>
    {
        this.html = html;
        onHtmlReady(html);
    });
}

Requesting the Dictionary Web Service

Now that we have the puzzle, we should provide the user with clues. Unfortunately, we can't use the same website that generated the crossword grid to provide us with the clues. In this case, we should look for some sort of "online English dictionary". I found a good one hosted by http://www.aonaware.com/, and I'm sure the readers looking for online dictionary resources will find it very helpful as well.

Image 6

As you can see, it's a SOAP webservice. The only method of those provided by the service that we need to call in the app is the DefineInDict, which requires:

  • The code of the dictionary
  • The word being searched for

In "dictionary id", we simply use wn, which means "Word Net". Word Net is one of the available dictionaries, and I think it's simple to use, compared to the others.

Image 7

Image 8

Here is the core of the dictionary webservice request: for each split word in the crossword grid, we do a request and when the web service returns a definition for a specific word, the callback method client_DefineInDictCompleted is called and the result is parsed:

C#
public class DictionaryHelper
{
    Dictionary<string, string> wordDict = new Dictionary<string, string>();
    public DictionaryHelper(Dictionary<string, string> wordDict)
    {
        this.wordDict = wordDict;
    }

    public void GetDictionaryEntries(Action<string, string, string> callback)
    {
        var client = new dictServiceRef.DictServiceSoapClient();

        client.DefineInDictCompleted +=
           new EventHandler<dictServiceRef.DefineInDictCompletedEventArgs>((s, e) =>
        {
            if (e.Error == null)
            {
                client_DefineInDictCompleted((string)e.UserState, e.Result, callback);
            }
        });

        foreach (var word in wordDict)
        {
            try
            {
                client.DefineInDictAsync("wn", word.Value,
                         string.Format("{0}|{1}", word.Key, word.Value));
            }
            catch
            {
            }
        }
    }

    void client_DefineInDictCompleted(string keyAndValue,
        WordDefinition wordDefinition, Action<string, string, string> callback)
    {
        var definitions = wordDefinition.Definitions;

        var dic = new Dictionary<string, string>();

        var split = keyAndValue.Split('|');
        var key = split[0];
        var value = split[1];
        if (definitions.Length == 0)
        {
            callback(key, value, "");
        }
        else
        {
            foreach (var definition in definitions)
            {
                if (definition.Dictionary.Id == "wn")
                {
                    var def = definition.WordDefinition.ToLower();
                    var postColon = def;

                    if (def.Split(':').Length > 1)
                        postColon = def.Split(':')[1];

                    var preSemicolon = postColon.Split(';')[0];
                    preSemicolon = preSemicolon
                        .Replace("\n", " ");

                    RegexOptions options = RegexOptions.None;
                    Regex regex = new Regex(@"[ ]{2,}", options);
                    preSemicolon = regex.Replace(preSemicolon, @" ");

                    var preOpenBrackets = preSemicolon.Split('[')[0];

                    var preNumber = preOpenBrackets.Split("2:".ToCharArray())[0];

                    preNumber = preNumber.Trim().Replace("\r\n", "");

                    var shortDefinition = preNumber;

                    if (def.StartsWith(string.Format("{0}\n     n", value.ToLower())))
                    {
                        shortDefinition += " (noun)";
                    }

                    callback(key, value, shortDefinition);
                    break;
                }
            }
        }
    }
}

Notice that we parse the definition so that the presented clue doesn't appear bloated for the user. Let's take for example the word "retrograde", and see how we parse it:

Image 9

Now look at how we show the shortened clue in our app:

Image 10

Selecting Words

Once the app makes all requests to the dictionary, the user is free to selects words and type in the letters. In order to facilitate the word selection, the app allows "swipe" gestures over the puzzle grid. The application then determines which word has been selected, according to a combination of the coordinates of the swipe start point and the swipe end point.

First, we have to subscribe to the FrameReported event of the System.Windows.Input.Touch.Touch class, so that we can intercept and handle the gesture we want to process (in this case, the swipe gesture).

C#
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    AfterEnteredPage();
    Touch.FrameReported += new TouchFrameEventHandler(Touch_FrameReported);
}

We can get all the gesture information through the TouchFrameEventArgs parameter of the FrameReported event. Each swipe gesture generates at least three distinct actions: TouchAction.Down, TouchAction.Move and TouchAction.Up. Then we gather these points and call a function called SelectSquaresByPosition to select the word accordingly:

C#
void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
    try
    {
        var touchPoint = e.GetPrimaryTouchPoint(grdTileContainer);
        if (touchPoint.Action == TouchAction.Down)
        {
            swipeDownPoint = touchPoint.Position;
        }
        else if (touchPoint.Action == TouchAction.Move)
        {
            swipeMovePoint = touchPoint.Position;
        }
        else if (touchPoint.Action == TouchAction.Up)
        {
            swipeUpPoint = touchPoint.Position;

            if (swipeDownPoint != new Point(0, 0) &&
                swipeMovePoint != new Point(0, 0))
            {
                boardViewModel.SelectSquaresByPosition(grdTileContainer.ActualWidth,
                grdTileContainer.ActualHeight, swipeDownPoint, swipeUpPoint);

                swipeDownPoint =
                swipeMovePoint =
                swipeUpPoint = new Point(0, 0);

                ShowClues();
            }
        }
    }
    catch (ArgumentException ex)
    {
    }
}

I tried to create a "smart" swipe gesture in the game, by selecting first only the empty cells of the word (so that the user doesn't have to retype all the letters). When the user swipes again over the same word, the app selects all the cells. Another swipe will select only the empty cells, and so on. These images illustrate the concept better than words:

First, we choose which word to select:

Image 11

Then we swipe over that word. Notice that only the empty squares got selected:

Image 12

By swiping again, we are informing the app that we want to select the whole word (and probably change it by retyping the whole word, not only the empty cells):

Image 13

And here is the code that does the magic. Notice that we first select only the empty cells under the condition that:

  • There is some empty square in that word which is unselected OR
  • All of the squares inside that word are already selected
C#
public void SelectSquaresByWordId(string wordId1)
{
    var isSomeSquareUnSelected =
        squares
            .Where(s => s.WordId.Split(',').Contains(wordId1)
                        && !s.IsSelected).Any();

    var isSomeEmptySquareUnSelected =
        squares
            .Where(s => s.WordId.Split(',').Contains(wordId1)
                        && (s.UserLetter ?? "").Trim() == ""
                        && !s.IsSelected).Any();

    if (isSomeEmptySquareUnSelected || !isSomeSquareUnSelected)
    {
        squares
            .ToList()
            .ForEach(s =>
            {
                var split = s.WordId.Split(',');
                s.IsSelected = split.Contains(wordId1) &&
                               (s.UserLetter ?? "").Trim() == "";
                squaresChangedCallback(squares,
                    new NotifyCollectionChangedEventArgs
                              (NotifyCollectionChangedAction.Add,
                     s, s.Row * size + s.Column));
            });
    }
    else
    {
        squares
            .ToList()
            .ForEach(s =>
            {
                var split = s.WordId.Split(',');
                s.IsSelected = split.Contains(wordId1);
                squaresChangedCallback(squares,
                    new NotifyCollectionChangedEventArgs
                              (NotifyCollectionChangedAction.Add,
                     s, s.Row * size + s.Column));
            });
    }

    var txt = "";

    if (clues.Count() > 0)
    {
        var selectedClue = clues.Where(c => c.WordId == wordId1);
        if (selectedClue.Any())
        {
            var clue = selectedClue.First();
            txt = clue.Definition;
        }
        TxtClue = txt.Trim();
    }
}

Final Considerations

I hope you enjoyed the app and the article. Please feel free to leave a comment with your opinion below!

History

  • 2012-06-30: Initial version
  • 2012-07-02: Size selection explained
  • 2012-07-03: Word selection explained

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Instructor / Trainer Alura Cursos Online
Brazil Brazil

Comments and Discussions

 
Questionexcellent work Pin
Rahul kumar2212-Mar-17 20:40
Rahul kumar2212-Mar-17 20:40 
Questionalternate links for crossword generator Pin
Member 107892731-May-14 19:42
Member 107892731-May-14 19:42 
GeneralMy vote of 5 Pin
Yusuf Uzun16-Aug-12 4:39
Yusuf Uzun16-Aug-12 4:39 
GeneralMy vote of 5 Pin
SoMad8-Aug-12 13:25
professionalSoMad8-Aug-12 13:25 
This really is excellent. Now I want to finish the stuff I am currently working on so I can try this out.
I am not a big fan of crossword puzzles, but that does not take anything away from your article.

Soren Madsen
GeneralMy vote of 5 Pin
asWorks2387920-Jul-12 6:03
asWorks2387920-Jul-12 6:03 
General_ive Pin
Meshack Musundi3-Jul-12 23:57
professionalMeshack Musundi3-Jul-12 23:57 
GeneralRe: _ive Pin
Marcelo Ricardo de Oliveira11-Jul-12 7:26
mvaMarcelo Ricardo de Oliveira11-Jul-12 7:26 
QuestionUhhhhhh Sweeet Pin
Dave Kerr3-Jul-12 21:34
mentorDave Kerr3-Jul-12 21:34 
AnswerRe: Uhhhhhh Sweeet Pin
Marcelo Ricardo de Oliveira11-Jul-12 7:25
mvaMarcelo Ricardo de Oliveira11-Jul-12 7:25 
GeneralMy vote of 5 Pin
Florian Rappl3-Jul-12 20:04
professionalFlorian Rappl3-Jul-12 20:04 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira11-Jul-12 7:25
mvaMarcelo Ricardo de Oliveira11-Jul-12 7:25 
GeneralMy vote of 5 Pin
Erich Ledesma2-Jul-12 22:03
Erich Ledesma2-Jul-12 22:03 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira3-Jul-12 4:49
mvaMarcelo Ricardo de Oliveira3-Jul-12 4:49 
QuestionAnother nice one Pin
Sacha Barber2-Jul-12 20:07
Sacha Barber2-Jul-12 20:07 
AnswerRe: Another nice one Pin
Marcelo Ricardo de Oliveira3-Jul-12 4:48
mvaMarcelo Ricardo de Oliveira3-Jul-12 4:48 
GeneralMy vote of 5 Pin
JF201530-Jun-12 22:24
JF201530-Jun-12 22:24 
GeneralRe: My vote of 5 Pin
Marcelo Ricardo de Oliveira3-Jul-12 4:42
mvaMarcelo Ricardo de Oliveira3-Jul-12 4:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.