Click here to Skip to main content
Click here to Skip to main content

Windows Phone Crosswords

, 3 Jul 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
Learn how to create a Windows Phone crosswords game taking advantage of online internet resources

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:

  • Windows Phone SDK 7.1

    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:

    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:

            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.

            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 provides 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=

    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=

    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=

    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 0's and 1's, where the zeroes represent the empty cells) into the expected plaing querystring:

    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 in 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 details 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:

            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:

        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.

        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.

    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 search 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.

    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 specifig word, the callback method client_DefineInDictCompleted is called and the result is parsed:

        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:

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

    Selecting Words

    Once the app makes all requests the 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).

        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:

            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:

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

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

    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

            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 have 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)

    Share

    About the Author

    Marcelo Ricardo de Oliveira
    Software Developer
    Brazil Brazil
    Marcelo Ricardo de Oliveira is a senior software developer who lives with his lovely wife Luciana and his little buddy and stepson Kauê in Guarulhos, Brazil, is co-founder of the Brazilian TV Guide TV Map and currently works for ILang Educação.
     
    He is often working with serious, enterprise projects, although in spare time he's trying to write fun Code Project articles involving WPF, Silverlight, XNA, HTML5 canvas, Windows Phone app development, game development and music.
     
    Published Windows Phone apps:
     
     
    Awards:
     
    CodeProject MVP 2012
    CodeProject MVP 2011
     
    Best Web Dev article of March 2013
    Best Web Dev article of August 2012
    Best Web Dev article of May 2012
    Best Mobile article of January 2012
    Best Mobile article of December 2011
    Best Mobile article of October 2011
    Best Web Dev article of September 2011
    Best Web Dev article of August 2011
    HTML5 / CSS3 Competition - Second Prize
    Best ASP.NET article of June 2011
    Best ASP.NET article of May 2011
    Best ASP.NET article of April 2011
    Best C# article of November 2010
    Best overall article of November 2010
    Best C# article of October 2010
    Best C# article of September 2010
    Best overall article of September 2010
    Best overall article of February 2010
    Best C# article of November 2009

    Comments and Discussions

     
    Questionalternate links for crossword generator PinmemberMember 107892731-May-14 20:42 
    GeneralMy vote of 5 PinmemberYusuf Uzun16-Aug-12 5:39 
    GeneralMy vote of 5 PinmemberSoMad8-Aug-12 14:25 
    GeneralMy vote of 5 PinmemberasWorks2387920-Jul-12 7:03 
    General_ive PinmvpMeshack Musundi4-Jul-12 0:57 
    GeneralRe: _ive PinmvpMarcelo Ricardo de Oliveira11-Jul-12 8:26 
    QuestionUhhhhhh Sweeet PinmvpDave Kerr3-Jul-12 22:34 
    AnswerRe: Uhhhhhh Sweeet PinmvpMarcelo Ricardo de Oliveira11-Jul-12 8:25 
    GeneralMy vote of 5 PinmemberFlorian Rappl3-Jul-12 21:04 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira11-Jul-12 8:25 
    GeneralMy vote of 5 PinmemberErich Ledesma2-Jul-12 23:03 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira3-Jul-12 5:49 
    QuestionAnother nice one PinmvpSacha Barber2-Jul-12 21:07 
    AnswerRe: Another nice one PinmvpMarcelo Ricardo de Oliveira3-Jul-12 5:48 
    GeneralMy vote of 5 PinmemberJF201530-Jun-12 23:24 
    GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira3-Jul-12 5:42 

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

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

    | Advertise | Privacy | Terms of Use | Mobile
    Web03 | 2.8.1411023.1 | Last Updated 4 Jul 2012
    Article Copyright 2012 by Marcelo Ricardo de Oliveira
    Everything else Copyright © CodeProject, 1999-2014
    Layout: fixed | fluid